mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
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:
81
website/client/assets/scss/create-task.scss
Normal file
81
website/client/assets/scss/create-task.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,6 @@ export default {
|
||||
if (!this.currentGroup) return false;
|
||||
return this.currentGroup.leader === this.user._id;
|
||||
},
|
||||
isManager () {
|
||||
if (!this.currentGroup) return false;
|
||||
return Boolean(this.currentGroup.managers[this.user._id]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -64,15 +64,24 @@
|
||||
.d-flex.align-items-center
|
||||
span(v-once) {{ $t('filter') }}
|
||||
.svg-icon.filter-icon(v-html="icons.filter")
|
||||
#create-dropdown.col-12.col-md-4
|
||||
b-dropdown.float-right(:right="true", :variant="'success'")
|
||||
.button-label(slot="button-content")
|
||||
.svg-icon.positive(v-html="icons.positive")
|
||||
| {{ $t('addTaskToGroupPlan') }}
|
||||
b-dropdown-item(v-for="type in columns", :key="type", @click="createTask(type)")
|
||||
span.dropdown-icon-item(v-once)
|
||||
span.svg-icon.inline(v-html="icons[type]")
|
||||
span.text {{$t(type)}}
|
||||
.create-task-area.d-flex(v-if='canCreateTasks')
|
||||
transition(name="slide-tasks-btns")
|
||||
.d-flex(v-if="openCreateBtn")
|
||||
.create-task-btn.rounded-btn(
|
||||
v-for="type in columns",
|
||||
:key="type",
|
||||
@click="createTask(type)",
|
||||
v-b-tooltip.hover.bottom="$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
|
||||
task-column.col-12.col-md-3(
|
||||
v-for="column in columns",
|
||||
@@ -81,12 +90,14 @@
|
||||
:taskListOverride='tasksByType[column]',
|
||||
v-on:editTask="editTask",
|
||||
v-on:loadGroupCompletedTodos="loadGroupCompletedTodos",
|
||||
v-on:taskDestroyed="taskDestroyed",
|
||||
:group='group',
|
||||
:searchText="searchText")
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/create-task.scss';
|
||||
|
||||
.user-tasks-page {
|
||||
padding-top: 31px;
|
||||
@@ -339,6 +350,10 @@ export default {
|
||||
|
||||
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: {
|
||||
async load () {
|
||||
@@ -400,6 +415,7 @@ export default {
|
||||
});
|
||||
},
|
||||
createTask (type) {
|
||||
this.openCreateBtn = false;
|
||||
this.taskFormPurpose = 'create';
|
||||
this.creatingTask = taskDefaults({type, text: ''}, this.user);
|
||||
this.workingTask = this.creatingTask;
|
||||
|
||||
@@ -349,10 +349,11 @@ import creatorIntro from '../creatorIntro';
|
||||
import InboxModal from '../userMenu/inbox.vue';
|
||||
import notificationMenu from './notificationsDropdown';
|
||||
import profileModal from '../userMenu/profileModal';
|
||||
import reportFlagModal from '../chat/reportFlagModal';
|
||||
import sendGemsModal from 'client/components/payments/sendGemsModal';
|
||||
import sync from 'client/mixins/sync';
|
||||
import userDropdown from './userDropdown';
|
||||
|
||||
import reportFlagModal from '../chat/reportFlagModal';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -364,6 +365,7 @@ export default {
|
||||
sendGemsModal,
|
||||
userDropdown,
|
||||
},
|
||||
mixins: [sync],
|
||||
data () {
|
||||
return {
|
||||
isUserDropdownOpen: false,
|
||||
@@ -403,14 +405,6 @@ export default {
|
||||
toggleUserDropdown () {
|
||||
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 () {
|
||||
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ base-notification(
|
||||
:has-icon="false",
|
||||
:notification="notification",
|
||||
@click="action",
|
||||
ref="taskApprovalNotification",
|
||||
)
|
||||
div(slot="content")
|
||||
div(v-html="notification.data.message")
|
||||
@@ -43,12 +44,12 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(this.$t('confirmApproval'))) return;
|
||||
|
||||
this.$store.dispatch('tasks:approve', {
|
||||
await this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.notification.data.groupTaskId,
|
||||
userId: this.notification.data.userId,
|
||||
});
|
||||
|
||||
this.$refs.taskApprovalNotification.remove();
|
||||
},
|
||||
async needsWork () {
|
||||
// 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;
|
||||
|
||||
this.$store.dispatch('tasks:needsWork', {
|
||||
await this.$store.dispatch('tasks:needsWork', {
|
||||
taskId: this.notification.data.groupTaskId,
|
||||
userId: this.notification.data.userId,
|
||||
});
|
||||
|
||||
this.$refs.taskApprovalNotification.remove();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -35,8 +35,10 @@ div
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import approvalModal from './approvalModal';
|
||||
import sync from 'client/mixins/sync';
|
||||
|
||||
export default {
|
||||
mixins: [sync],
|
||||
props: ['task', 'group'],
|
||||
components: {
|
||||
approvalModal,
|
||||
@@ -91,11 +93,12 @@ export default {
|
||||
taskId = this.task.group.taskId;
|
||||
}
|
||||
|
||||
this.$store.dispatch('tasks:assignTask', {
|
||||
await this.$store.dispatch('tasks:assignTask', {
|
||||
taskId,
|
||||
userId: this.user._id,
|
||||
});
|
||||
this.task.group.assignedUsers.push(this.user._id);
|
||||
this.sync();
|
||||
},
|
||||
async unassign () {
|
||||
if (!confirm(this.$t('confirmUnClaim'))) return;
|
||||
@@ -106,15 +109,16 @@ export default {
|
||||
taskId = this.task.group.taskId;
|
||||
}
|
||||
|
||||
this.$store.dispatch('tasks:unassignTask', {
|
||||
await this.$store.dispatch('tasks:unassignTask', {
|
||||
taskId,
|
||||
userId: this.user._id,
|
||||
});
|
||||
let index = this.task.group.assignedUsers.indexOf(this.user._id);
|
||||
this.task.group.assignedUsers.splice(index, 1);
|
||||
|
||||
this.sync();
|
||||
},
|
||||
approve () {
|
||||
if (!confirm(this.$t('confirmApproval'))) return;
|
||||
let userIdToApprove = this.task.group.assignedUsers[0];
|
||||
this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.task._id,
|
||||
|
||||
@@ -24,7 +24,6 @@ export default {
|
||||
props: ['task'],
|
||||
methods: {
|
||||
approve (index) {
|
||||
if (!confirm(this.$t('confirmApproval'))) return;
|
||||
let userIdToApprove = this.task.group.assignedUsers[index];
|
||||
this.$store.dispatch('tasks:approve', {
|
||||
taskId: this.task._id,
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
@update='taskSorted',
|
||||
@start="isDragging(true)",
|
||||
@end="isDragging(false)",
|
||||
:options='{disabled: activeFilter.label === "scheduled", scrollSensitivity: 64}',
|
||||
:options='{disabled: activeFilter.label === "scheduled" || !isUser, scrollSensitivity: 64}',
|
||||
)
|
||||
task(
|
||||
v-for="task in taskList",
|
||||
@@ -48,6 +48,7 @@
|
||||
@editTask="editTask",
|
||||
@moveTo="moveTo",
|
||||
:group='group',
|
||||
v-on:taskDestroyed='taskDestroyed'
|
||||
)
|
||||
template(v-if="hasRewardsList")
|
||||
draggable.reward-items(
|
||||
@@ -691,6 +692,9 @@ export default {
|
||||
document.documentElement.classList.remove('draggable-cursor');
|
||||
}
|
||||
},
|
||||
taskDestroyed (task) {
|
||||
this.$emit('taskDestroyed', task);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
.d-flex.justify-content-between
|
||||
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
|
||||
menu-dropdown.task-dropdown(
|
||||
v-if="isUser && !isRunningYesterdailies",
|
||||
v-if="!isRunningYesterdailies",
|
||||
:right="task.type === 'reward'",
|
||||
ref="taskDropdown",
|
||||
v-b-tooltip.hover.top="$t('options')"
|
||||
@@ -29,11 +29,11 @@
|
||||
span.dropdown-icon-item
|
||||
span.svg-icon.inline.edit-icon(v-html="icons.edit")
|
||||
span.text {{ $t('edit') }}
|
||||
.dropdown-item(@click="moveToTop")
|
||||
.dropdown-item(v-if='isUser', @click="moveToTop")
|
||||
span.dropdown-icon-item
|
||||
span.svg-icon.inline.push-to-top(v-html="icons.top")
|
||||
span.text {{ $t('taskToTop') }}
|
||||
.dropdown-item(@click="moveToBottom")
|
||||
.dropdown-item(v-if='isUser', @click="moveToBottom")
|
||||
span.dropdown-icon-item
|
||||
span.svg-icon.inline.push-to-bottom(v-html="icons.bottom")
|
||||
span.text {{ $t('taskToBottom') }}
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/create-task.scss';
|
||||
|
||||
.user-tasks-page {
|
||||
padding-top: 16px;
|
||||
@@ -118,87 +119,6 @@
|
||||
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 {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
12
website/client/mixins/sync.js
Normal file
12
website/client/mixins/sync.js
Normal 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');
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -484,7 +484,7 @@
|
||||
"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 Group’s member list.",
|
||||
"goToTaskBoard": "Go to Task Board",
|
||||
"sharedCompletion": "Shared Completion",
|
||||
"sharedCompletion": "Completion Conditon",
|
||||
"recurringCompletion": "None - Group task does not complete",
|
||||
"singleCompletion": "Single - Completes when any assigned user finishes",
|
||||
"allAssignedCompletion": "All - Completes when all assigned users finish"
|
||||
|
||||
@@ -1418,6 +1418,10 @@ schema.methods.removeTask = async function groupRemoveTask (task) {
|
||||
}, {
|
||||
$set: {'group.broken': 'TASK_DELETED'},
|
||||
}, {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
|
||||
|
||||
Reference in New Issue
Block a user