mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Sept 22 fixes (#9065)
* Removed lingering checklist * Added another party data check * Added move cursor on hover * Removed task locally * Prevented user from being able to delete an active challenge task * Reset tasks when viewing member progress * Prevented challenge owners from adding checklists * Hide challenges columns with no tasks * Add error translations * Added markdown to challenge description * Allowed leader to rejoin challenge * Replaced description with summary * Fixed delete logic * Added author * Added loading message * Added load more * Added default sub * Fixed remove all * Added lint
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
h1 {{challenge.name}}
|
||||
div
|
||||
strong(v-once) {{$t('createdBy')}}:
|
||||
span {{challenge.author}}
|
||||
span {{challenge.leader.profile.name}}
|
||||
// @TODO: make challenge.author a variable inside the createdBy string (helps with RTL languages)
|
||||
// @TODO: Implement in V2 strong.margin-left(v-once)
|
||||
.svg-icon.calendar-icon(v-html="icons.calendarIcon")
|
||||
@@ -42,10 +42,11 @@
|
||||
:type="column",
|
||||
:key="column",
|
||||
:taskListOverride='tasksByType[column]',
|
||||
v-on:editTask="editTask")
|
||||
v-on:editTask="editTask",
|
||||
v-if='tasksByType[column].length > 0')
|
||||
.col-4.sidebar.standard-page
|
||||
.acitons
|
||||
div(v-if='!isMember && !isLeader')
|
||||
div(v-if='canJoin')
|
||||
button.btn.btn-success(v-once, @click='joinChallenge()') {{$t('joinChallenge')}}
|
||||
div(v-if='isMember')
|
||||
button.btn.btn-danger(v-once, @click='leaveChallenge()') {{$t('leaveChallenge')}}
|
||||
@@ -61,6 +62,7 @@
|
||||
:challengeId="challengeId",
|
||||
v-on:taskCreated='taskCreated',
|
||||
v-on:taskEdited='taskEdited',
|
||||
@taskDestroyed='taskDestroyed'
|
||||
)
|
||||
div(v-if='isLeader')
|
||||
button.btn.btn-secondary(v-once, @click='edit()') {{$t('editChallenge')}}
|
||||
@@ -74,7 +76,7 @@
|
||||
h2 {{$t('challengeSummary')}}
|
||||
p {{challenge.summary}}
|
||||
h2 {{$t('challengeDescription')}}
|
||||
p {{challenge.description}}
|
||||
p(v-markdown='challenge.description')
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@@ -178,6 +180,7 @@ import { mapState } from 'client/libs/store';
|
||||
import closeChallengeModal from './closeChallengeModal';
|
||||
import Column from '../tasks/column';
|
||||
import TaskModal from '../tasks/taskModal';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import challengeModal from './challengeModal';
|
||||
import challengeMemberProgressModal from './challengeMemberProgressModal';
|
||||
|
||||
@@ -189,6 +192,9 @@ import calendarIcon from 'assets/svg/calendar.svg';
|
||||
|
||||
export default {
|
||||
props: ['challengeId'],
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
closeChallengeModal,
|
||||
challengeModal,
|
||||
@@ -232,6 +238,9 @@ export default {
|
||||
if (!this.challenge.leader) return false;
|
||||
return this.user._id === this.challenge.leader._id;
|
||||
},
|
||||
canJoin () {
|
||||
return !this.isMember;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (!this.searchId) this.searchId = this.challengeId;
|
||||
@@ -327,7 +336,14 @@ export default {
|
||||
});
|
||||
this.tasksByType[task.type].splice(index, 1, task);
|
||||
},
|
||||
taskDestroyed (task) {
|
||||
let index = findIndex(this.tasksByType[task.type], (taskItem) => {
|
||||
return taskItem._id === task._id;
|
||||
});
|
||||
this.tasksByType[task.type].splice(index, 1);
|
||||
},
|
||||
showMemberModal () {
|
||||
this.$store.state.memberModalOptions.challengeId = this.challenge._id;
|
||||
this.$store.state.memberModalOptions.groupId = 'challenge'; // @TODO: change these terrible settings
|
||||
this.$store.state.memberModalOptions.group = this.group;
|
||||
this.$store.state.memberModalOptions.viewingMembers = this.members;
|
||||
|
||||
@@ -33,6 +33,13 @@ export default {
|
||||
watch: {
|
||||
async memberId (id) {
|
||||
if (!id) return;
|
||||
this.tasksByType = {
|
||||
habit: [],
|
||||
daily: [],
|
||||
todo: [],
|
||||
reward: [],
|
||||
};
|
||||
|
||||
let response = await axios.get(`/api/v3/challenges/${this.challengeId}/members/${this.memberId}`);
|
||||
let tasks = response.data.data.tasks;
|
||||
tasks.forEach((task) => {
|
||||
|
||||
@@ -360,41 +360,44 @@ export default {
|
||||
},
|
||||
async createChallenge () {
|
||||
// @TODO: improve error handling, add it to updateChallenge, make errors translatable. Suggestion: `<% fieldName %> is required` where possible, where `fieldName` is inserted as the translatable string that's used for the field header.
|
||||
let errors = '';
|
||||
if (!this.workingChallenge.name) errors += 'Name is required\n';
|
||||
if (this.workingChallenge.shortName.length < MIN_SHORTNAME_SIZE_FOR_CHALLENGES) errors += 'Tag name is too short\n';
|
||||
if (!this.workingChallenge.summary) errors += 'Summary is required\n';
|
||||
if (this.workingChallenge.summary.length > MAX_SUMMARY_SIZE_FOR_CHALLENGES) errors += 'Summary is too long\n';
|
||||
if (!this.workingChallenge.description) errors += 'Description is required\n';
|
||||
if (!this.workingChallenge.group) errors += 'Location of challenge is required ("Add to")\n';
|
||||
if (!this.workingChallenge.categories || this.workingChallenge.categories.length === 0) errors += 'One or more categories must be selected\n';
|
||||
if (errors) {
|
||||
alert(errors);
|
||||
} else {
|
||||
this.workingChallenge.timestamp = new Date().getTime();
|
||||
let categoryKeys = this.workingChallenge.categories;
|
||||
let serverCategories = [];
|
||||
categoryKeys.forEach(key => {
|
||||
let catName = this.categoriesHashByKey[key];
|
||||
serverCategories.push({
|
||||
slug: key,
|
||||
name: catName,
|
||||
});
|
||||
});
|
||||
this.workingChallenge.categories = serverCategories;
|
||||
let errors = [];
|
||||
|
||||
let challenge = await this.$store.dispatch('challenges:createChallenge', {challenge: this.workingChallenge});
|
||||
// @TODO: When to remove from guild instead?
|
||||
this.user.balance -= this.workingChallenge.prize / 4;
|
||||
if (!this.workingChallenge.name) errors.push(this.$t('nameRequired'));
|
||||
if (this.workingChallenge.shortName.length < MIN_SHORTNAME_SIZE_FOR_CHALLENGES) errors.push(this.$t('tagTooShort'));
|
||||
if (!this.workingChallenge.summary) errors.push(this.$t('summaryRequired'));
|
||||
if (this.workingChallenge.summary.length > MAX_SUMMARY_SIZE_FOR_CHALLENGES) errors.push(this.$t('summaryTooLong'));
|
||||
if (!this.workingChallenge.description) errors.push(this.$t('descriptionRequired'));
|
||||
if (!this.workingChallenge.group) errors.push(this.$t('locationRequired'));
|
||||
if (!this.workingChallenge.categories || this.workingChallenge.categories.length === 0) errors.push(this.$t('categoiresRequired'));
|
||||
|
||||
this.$emit('createChallenge', challenge);
|
||||
this.resetWorkingChallenge();
|
||||
|
||||
if (this.cloning) this.$store.state.challengeOptions.cloning = true;
|
||||
|
||||
this.$root.$emit('hide::modal', 'challenge-modal');
|
||||
this.$router.push(`/challenges/${challenge._id}`);
|
||||
if (errors.length > 0) {
|
||||
alert(errors.join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.workingChallenge.timestamp = new Date().getTime();
|
||||
let categoryKeys = this.workingChallenge.categories;
|
||||
let serverCategories = [];
|
||||
categoryKeys.forEach(key => {
|
||||
let catName = this.categoriesHashByKey[key];
|
||||
serverCategories.push({
|
||||
slug: key,
|
||||
name: catName,
|
||||
});
|
||||
});
|
||||
this.workingChallenge.categories = serverCategories;
|
||||
|
||||
let challenge = await this.$store.dispatch('challenges:createChallenge', {challenge: this.workingChallenge});
|
||||
// @TODO: When to remove from guild instead?
|
||||
this.user.balance -= this.workingChallenge.prize / 4;
|
||||
|
||||
this.$emit('createChallenge', challenge);
|
||||
this.resetWorkingChallenge();
|
||||
|
||||
if (this.cloning) this.$store.state.challengeOptions.cloning = true;
|
||||
|
||||
this.$root.$emit('hide::modal', 'challenge-modal');
|
||||
this.$router.push(`/challenges/${challenge._id}`);
|
||||
},
|
||||
updateChallenge () {
|
||||
let categoryKeys = this.workingChallenge.categories;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
.row.header-row
|
||||
.col-md-8.text-left
|
||||
h1(v-once) {{$t('findChallenges')}}
|
||||
h2(v-if='loading') {{ $t('loading') }}
|
||||
.col-md-4
|
||||
// @TODO: implement sorting span.dropdown-label {{ $t('sortBy') }}
|
||||
b-dropdown(:text="$t('sort')", right=true)
|
||||
@@ -63,6 +64,7 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: true,
|
||||
icons: Object.freeze({
|
||||
positiveIcon,
|
||||
}),
|
||||
@@ -125,7 +127,9 @@ export default {
|
||||
this.$root.$emit('show::modal', 'challenge-modal');
|
||||
},
|
||||
async loadchallanges () {
|
||||
this.loading = true;
|
||||
this.challenges = await this.$store.dispatch('challenges:getUserChallenges');
|
||||
this.loading = false;
|
||||
},
|
||||
challengeCreated (challenge) {
|
||||
this.challenges.push(challenge);
|
||||
|
||||
@@ -12,7 +12,7 @@ div
|
||||
.col-9
|
||||
router-link.title(:to="{ name: 'challenge', params: { challengeId: challenge._id } }")
|
||||
strong {{challenge.name}}
|
||||
p {{challenge.description}}
|
||||
p {{challenge.summary || challenge.name}}
|
||||
div
|
||||
.svg-icon.member-icon(v-html="icons.memberIcon")
|
||||
.member-count {{challenge.memberCount}}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template lang="pug">
|
||||
// @TODO: Move this to a member directory
|
||||
div
|
||||
b-modal#members-modal(:title="$t('createGuild')", size='md')
|
||||
.header-wrap(slot="modal-header")
|
||||
@@ -41,6 +42,9 @@ div
|
||||
span.dropdown-icon-item
|
||||
.svg-icon.inline(v-html="icons.removeIcon")
|
||||
span.text {{$t('removeManager2')}}
|
||||
.row(v-if='groupId === "challenge"')
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary(@click='loadMoreMembers()') {{ $t('loadMore') }}
|
||||
.row.gradient(v-if='members.length > 3')
|
||||
</template>
|
||||
|
||||
@@ -203,6 +207,9 @@ export default {
|
||||
groupId () {
|
||||
return this.$store.state.memberModalOptions.groupId || this.group._id;
|
||||
},
|
||||
challengeId () {
|
||||
return this.$store.state.memberModalOptions.challengeId;
|
||||
},
|
||||
sortedMembers () {
|
||||
let sortedMembers = this.members;
|
||||
if (!this.sortOption) return sortedMembers;
|
||||
@@ -312,6 +319,17 @@ export default {
|
||||
sort (option) {
|
||||
this.sortOption = option;
|
||||
},
|
||||
async loadMoreMembers () {
|
||||
const lastMember = this.members[this.members.length - 1];
|
||||
if (!lastMember) return;
|
||||
|
||||
let newMembers = await this.$store.dispatch('members:getChallengeMembers', {
|
||||
challengeId: this.challengeId,
|
||||
lastMemberId: lastMember._id,
|
||||
});
|
||||
|
||||
this.members = this.members.concat(newMembers);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
<template lang="pug">
|
||||
form(
|
||||
v-if="task",
|
||||
@submit.stop.prevent="submit()",
|
||||
)
|
||||
b-modal#task-modal(
|
||||
size="sm",
|
||||
@hidden="onClose()",
|
||||
)
|
||||
.task-modal-header(
|
||||
slot="modal-header",
|
||||
:class="[cssClass]",
|
||||
)
|
||||
form(v-if="task", @submit.stop.prevent="submit()")
|
||||
b-modal#task-modal(size="sm", @hidden="onClose()")
|
||||
.task-modal-header(slot="modal-header", :class="[cssClass]")
|
||||
.clearfix
|
||||
h1.float-left {{ title }}
|
||||
.float-right.d-flex.align-items-center
|
||||
@@ -27,10 +18,9 @@
|
||||
label(v-once) {{ $t('cost') }}
|
||||
input(type="number", v-model="task.value", required, min="0")
|
||||
.svg-icon.gold(v-html="icons.gold")
|
||||
.option(v-if="['daily', 'todo'].indexOf(task.type) > -1")
|
||||
.option(v-if="checklistEnabled")
|
||||
label(v-once) {{ $t('checklist') }}
|
||||
br
|
||||
| {{checklist}}
|
||||
div(v-sortable='', @onsort='sortedChecklist')
|
||||
.inline-edit-input-group.checklist-group.input-group(v-for="(item, $index) in checklist")
|
||||
input.inline-edit-input.checklist-item.form-control(type="text", v-model="item.text")
|
||||
@@ -178,7 +168,7 @@
|
||||
|
||||
.task-modal-footer(slot="modal-footer")
|
||||
span.cancel-task-btn(v-once, v-if="purpose === 'create'", @click="cancel()") {{ $t('cancel') }}
|
||||
span.delete-task-btn(v-once, v-else, @click="destroy()") {{ $t('delete') }}
|
||||
span.delete-task-btn(v-once, v-if='canDelete', @click="destroy()") {{ $t('delete') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -354,6 +344,10 @@
|
||||
background-size: 10px 10px;
|
||||
background-image: url(~client/assets/svg/for-css/positive.svg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-task-btn, .cancel-task-btn {
|
||||
@@ -486,6 +480,18 @@ export default {
|
||||
user: 'user.data',
|
||||
dayMapping: 'constants.DAY_MAPPING',
|
||||
}),
|
||||
checklistEnabled () {
|
||||
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
|
||||
},
|
||||
isOriginalChallengeTask () {
|
||||
let isUserChallenge = Boolean(this.task.userId);
|
||||
return !isUserChallenge && (this.challengeId || this.task.challenge && this.task.challenge.id);
|
||||
},
|
||||
canDelete () {
|
||||
let isUserChallenge = Boolean(this.task.userId);
|
||||
let activeChallenge = isUserChallenge && this.task.challenge && this.task.challenge.id && !this.task.challenge.broken;
|
||||
return this.purpose !== 'create' && !activeChallenge;
|
||||
},
|
||||
title () {
|
||||
const type = this.$t(this.task.type);
|
||||
return this.$t(this.purpose === 'edit' ? 'editATask' : 'createTask', {type});
|
||||
@@ -620,6 +626,7 @@ export default {
|
||||
destroy () {
|
||||
if (!confirm('Are you sure you want to delete this task?')) return;
|
||||
this.destroyTask(this.task);
|
||||
this.$emit('taskDestroyed', this.task);
|
||||
this.$root.$emit('hide::modal', 'task-modal');
|
||||
},
|
||||
cancel () {
|
||||
|
||||
@@ -21,7 +21,11 @@ export default {
|
||||
return `/paypal/subscribe?_id=${this.credentials.API_ID}&apiToken=${this.credentials.API_TOKEN}&sub=${this.subscriptionPlan}`;
|
||||
},
|
||||
paypalPurchaseLink () {
|
||||
if (!this.subscription) return '';
|
||||
if (!this.subscription) {
|
||||
this.subscription = {
|
||||
key: 'basic_earned',
|
||||
};
|
||||
}
|
||||
let couponString = '';
|
||||
if (this.subscription.coupon) couponString = `&coupon=${this.subscription.coupon}`;
|
||||
return `/paypal/subscribe?_id=${this.credentials.API_ID}&apiToken=${this.credentials.API_TOKEN}&sub=${this.subscription.key}${couponString}`;
|
||||
|
||||
@@ -17,10 +17,9 @@ export async function joinChallenge (store, payload) {
|
||||
}
|
||||
|
||||
export async function leaveChallenge (store, payload) {
|
||||
let response = await axios.post(`/api/v3/challenges/${payload.challengeId}/leave`, {
|
||||
data: {
|
||||
keep: payload.keep,
|
||||
},
|
||||
let url = `/api/v3/challenges/${payload.challengeId}/leave`;
|
||||
let response = await axios.post(url, {
|
||||
keep: payload.keep,
|
||||
});
|
||||
|
||||
return response.data.data;
|
||||
|
||||
@@ -27,6 +27,11 @@ export async function getGroupInvites (store, payload) {
|
||||
|
||||
export async function getChallengeMembers (store, payload) {
|
||||
let url = `${apiV3Prefix}/challenges/${payload.challengeId}/members?includeAllPublicFields=true`;
|
||||
|
||||
if (payload.lastMemberId) {
|
||||
url += `&lastId=${payload.lastMemberId}`;
|
||||
}
|
||||
|
||||
let response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function sendAction (store, payload) {
|
||||
partySize: store.state.party.members.data.length,
|
||||
};
|
||||
|
||||
if (store.state.party) {
|
||||
if (store.state.party && store.state.party.data) {
|
||||
partyData = {
|
||||
partyID: store.state.party.data._id,
|
||||
partySize: store.state.party.data.memberCount,
|
||||
|
||||
@@ -110,6 +110,7 @@ export default function () {
|
||||
memberModalOptions: {
|
||||
viewingMembers: [],
|
||||
groupId: '',
|
||||
challengeId: '',
|
||||
group: {},
|
||||
},
|
||||
openedItemRows: [],
|
||||
|
||||
@@ -116,5 +116,12 @@
|
||||
"haveNoChallenges": "You don't have any Challenges",
|
||||
"loadMore": "Load More",
|
||||
"exportChallengeCsv": "Export Challenge",
|
||||
"editingChallenge": "Editing Challenge"
|
||||
"editingChallenge": "Editing Challenge",
|
||||
"nameRequired": "Name is required",
|
||||
"tagTooShort": "Tag name is too short",
|
||||
"summaryRequired": "Summary is required",
|
||||
"summaryTooLong": "Summary is too long",
|
||||
"descriptionRequired": "Description is required",
|
||||
"locationRequired": "Location of challenge is required ('Add to')",
|
||||
"categoiresRequired": "One or more categories must be selected"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user