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:
Keith Holliday
2017-09-22 16:47:16 -05:00
committed by GitHub
parent 6fcf739c89
commit 6edd1a1fa5
13 changed files with 130 additions and 59 deletions

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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}}

View File

@@ -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>

View File

@@ -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 () {

View File

@@ -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}`;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -110,6 +110,7 @@ export default function () {
memberModalOptions: {
viewingMembers: [],
groupId: '',
challengeId: '',
group: {},
},
openedItemRows: [],

View File

@@ -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"
}