mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
Feature: new "report a bug" modal (#13530)
* WIP: report a bug api/ui * fix lint * add USER_USERNAME * extend sendTxn tests / checks + fix bug report email * fix lint * add more checks to sendTxn - fix bug-report variables * fix lint / ci * fix test: reset email config url * fix test stub * fix tests * refactor the variables checks * lint. * move bug-report page as a modal * send user_email to the email * show true/false instead 1/0 * fix issues * fix footer report bug email if not logged in * fix styles/margins * prefill user's email * show facebook email if local email not existing * bugReportSuccessModal.vue * add BROWSER_UA to mail properties * extract bugReportLogic to its own lib file for unit test * test api validators * fix lint
This commit is contained in:
57
test/api/unit/libs/bug-report.test.js
Normal file
57
test/api/unit/libs/bug-report.test.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/* eslint-disable global-require */
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import { generateUser } from '../../../helpers/api-unit.helper';
|
||||||
|
import * as emailLib from '../../../../website/server/libs/email';
|
||||||
|
import { bugReportLogic } from '../../../../website/server/libs/bug-report';
|
||||||
|
|
||||||
|
describe('bug-report', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox.stub(emailLib, 'sendTxn').returns(Promise.resolve());
|
||||||
|
|
||||||
|
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfGetStub.withArgs('ADMIN_EMAIL').returns('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends a mail using sendTxn', async () => {
|
||||||
|
const userId = '2b58daeb-bc50-4a83-b5d3-4ac52c7c0608';
|
||||||
|
const userMail = 'me@me.com';
|
||||||
|
const userMessage = 'The power is over 9000, please fix it';
|
||||||
|
const userAgent = 'The UserAgent with a bunch of weird browser engine levels';
|
||||||
|
|
||||||
|
const user = generateUser({
|
||||||
|
_id: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await bugReportLogic(
|
||||||
|
user, userMail, userMessage, userAgent,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(emailLib.sendTxn).to.be.called;
|
||||||
|
expect(result).to.deep.equal({
|
||||||
|
sendMailResult: undefined,
|
||||||
|
emailData: {
|
||||||
|
BROWSER_UA: userAgent,
|
||||||
|
REPORT_MSG: userMessage,
|
||||||
|
USER_CLASS: 'warrior',
|
||||||
|
USER_CONSECUTIVE_MONTHS: 0,
|
||||||
|
USER_COSTUME: 'false',
|
||||||
|
USER_CUSTOMER_ID: undefined,
|
||||||
|
USER_CUSTOM_DAY: 0,
|
||||||
|
USER_DAILIES_PAUSED: 'false',
|
||||||
|
USER_EMAIL: userMail,
|
||||||
|
USER_HOURGLASSES: 0,
|
||||||
|
USER_ID: userId,
|
||||||
|
USER_LEVEL: 1,
|
||||||
|
USER_OFFSET_MONTHS: 0,
|
||||||
|
USER_PAYMENT_PLATFORM: undefined,
|
||||||
|
USER_SUBSCRIPTION: undefined,
|
||||||
|
USER_TIMEZONE_OFFSET: 0,
|
||||||
|
USER_USERNAME: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -148,9 +148,18 @@ describe('emails', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sendTxnEmail', () => {
|
describe('sendTxn', () => {
|
||||||
|
let sendTxn = null;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox.stub(got, 'post').returns(defer().promise);
|
sandbox.stub(got, 'post').returns(defer().promise);
|
||||||
|
|
||||||
|
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfGetStub.withArgs('IS_PROD').returns(true);
|
||||||
|
nconfGetStub.withArgs('BASE_URL').returns('BASE_URL');
|
||||||
|
|
||||||
|
const attachEmail = requireAgain(pathToEmailLib);
|
||||||
|
sendTxn = attachEmail.sendTxn;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -158,16 +167,14 @@ describe('emails', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can send a txn email to one recipient', () => {
|
it('can send a txn email to one recipient', () => {
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
|
||||||
const attachEmail = requireAgain(pathToEmailLib);
|
|
||||||
const sendTxnEmail = attachEmail.sendTxn;
|
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
name: 'my name',
|
name: 'my name',
|
||||||
email: 'my@email',
|
email: 'my@email',
|
||||||
};
|
};
|
||||||
|
|
||||||
sendTxnEmail(mailingInfo, emailType);
|
sendTxn(mailingInfo, emailType);
|
||||||
|
expect(got.post).to.be.called;
|
||||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
@@ -179,27 +186,77 @@ describe('emails', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not send email if address is missing', () => {
|
it('does not send email if address is missing', () => {
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
|
||||||
const attachEmail = requireAgain(pathToEmailLib);
|
|
||||||
const sendTxnEmail = attachEmail.sendTxn;
|
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
name: 'my name',
|
name: 'my name',
|
||||||
// email: 'my@email',
|
// email: 'my@email',
|
||||||
};
|
};
|
||||||
|
|
||||||
sendTxnEmail(mailingInfo, emailType);
|
sendTxn(mailingInfo, emailType);
|
||||||
expect(got.post).not.to.be.called;
|
expect(got.post).not.to.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws error when mail target is only a string', () => {
|
||||||
|
const emailType = 'an email type';
|
||||||
|
const mailingInfo = 'my email';
|
||||||
|
|
||||||
|
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error when mail target has no _id or email', () => {
|
||||||
|
const emailType = 'an email type';
|
||||||
|
const mailingInfo = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error when variables not an array', () => {
|
||||||
|
const emailType = 'an email type';
|
||||||
|
const mailingInfo = {
|
||||||
|
name: 'my name',
|
||||||
|
email: 'my@email',
|
||||||
|
};
|
||||||
|
const variables = {};
|
||||||
|
|
||||||
|
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
||||||
|
});
|
||||||
|
it('throws error when variables array not contain name/content', () => {
|
||||||
|
const emailType = 'an email type';
|
||||||
|
const mailingInfo = {
|
||||||
|
name: 'my name',
|
||||||
|
email: 'my@email',
|
||||||
|
};
|
||||||
|
const variables = [
|
||||||
|
{
|
||||||
|
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
||||||
|
});
|
||||||
|
it('throws no error when variables array contain name but no content', () => {
|
||||||
|
const emailType = 'an email type';
|
||||||
|
const mailingInfo = {
|
||||||
|
name: 'my name',
|
||||||
|
email: 'my@email',
|
||||||
|
};
|
||||||
|
const variables = [
|
||||||
|
{
|
||||||
|
name: 'MY_VAR',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(sendTxn(mailingInfo, emailType, variables)).to.not.throw;
|
||||||
|
});
|
||||||
|
|
||||||
it('uses getUserInfo in case of user data', () => {
|
it('uses getUserInfo in case of user data', () => {
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
|
||||||
const attachEmail = requireAgain(pathToEmailLib);
|
|
||||||
const sendTxnEmail = attachEmail.sendTxn;
|
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = getUser();
|
const mailingInfo = getUser();
|
||||||
|
|
||||||
sendTxnEmail(mailingInfo, emailType);
|
sendTxn(mailingInfo, emailType);
|
||||||
|
expect(got.post).to.be.called;
|
||||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
@@ -211,17 +268,15 @@ describe('emails', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sends email with some default variables', () => {
|
it('sends email with some default variables', () => {
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
|
||||||
const attachEmail = requireAgain(pathToEmailLib);
|
|
||||||
const sendTxnEmail = attachEmail.sendTxn;
|
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
name: 'my name',
|
name: 'my name',
|
||||||
email: 'my@email',
|
email: 'my@email',
|
||||||
};
|
};
|
||||||
const variables = [1, 2, 3];
|
const variables = [];
|
||||||
|
|
||||||
sendTxnEmail(mailingInfo, emailType, variables);
|
sendTxn(mailingInfo, emailType, variables);
|
||||||
|
expect(got.post).to.be.called;
|
||||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||||
json: {
|
json: {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
49
test/api/v4/POST-bug-report.test.js
Normal file
49
test/api/v4/POST-bug-report.test.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
describe('POST /bug-report', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when message is not added', async () => {
|
||||||
|
await expect(user.post('/bug-report', {
|
||||||
|
message: '',
|
||||||
|
}))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
// seems it is not possible to get the real error message
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when email is not added', async () => {
|
||||||
|
await expect(user.post('/bug-report', {
|
||||||
|
message: 'message',
|
||||||
|
email: '',
|
||||||
|
}))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
// seems it is not possible to get the real error message
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when email is not valid', async () => {
|
||||||
|
await expect(user.post('/bug-report', {
|
||||||
|
message: 'message',
|
||||||
|
email: 'notamail',
|
||||||
|
}))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
// seems it is not possible to get the real error message
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,6 +40,18 @@ store.state.user.data = {
|
|||||||
preferences: {
|
preferences: {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
// email: 'example@example.com',
|
||||||
|
},
|
||||||
|
facebook: {
|
||||||
|
emails: [
|
||||||
|
{
|
||||||
|
value: 'test@test.de',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Vue.prototype.$store = store;
|
Vue.prototype.$store = store;
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
<payments-success-modal />
|
<payments-success-modal />
|
||||||
<sub-cancel-modal-confirm v-if="isUserLoaded" />
|
<sub-cancel-modal-confirm v-if="isUserLoaded" />
|
||||||
<sub-canceled-modal v-if="isUserLoaded" />
|
<sub-canceled-modal v-if="isUserLoaded" />
|
||||||
|
<bug-report-modal v-if="isUserLoaded" />
|
||||||
|
<bug-report-success-modal v-if="isUserLoaded" />
|
||||||
<snackbars />
|
<snackbars />
|
||||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -177,6 +179,10 @@ import {
|
|||||||
removeLocalSetting,
|
removeLocalSetting,
|
||||||
} from '@/libs/userlocalManager';
|
} from '@/libs/userlocalManager';
|
||||||
|
|
||||||
|
const bugReportModal = () => import(/* webpackChunkName: "bug-report-modal" */'@/components/bugReportModal');
|
||||||
|
const bugReportSuccessModal = () => import(/* webpackChunkName: "bug-report-success-modal" */'@/components/bugReportSuccessModal');
|
||||||
|
|
||||||
|
|
||||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -196,6 +202,8 @@ export default {
|
|||||||
paymentsSuccessModal,
|
paymentsSuccessModal,
|
||||||
subCancelModalConfirm,
|
subCancelModalConfirm,
|
||||||
subCanceledModal,
|
subCanceledModal,
|
||||||
|
bugReportModal,
|
||||||
|
bugReportSuccessModal,
|
||||||
},
|
},
|
||||||
mixins: [notifications, spellsMixin],
|
mixins: [notifications, spellsMixin],
|
||||||
data () {
|
data () {
|
||||||
|
|||||||
6
website/client/src/assets/svg/check_circle.svg
Normal file
6
website/client/src/assets/svg/check_circle.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<circle fill="#1CA372" cx="32" cy="32" r="32"/>
|
||||||
|
<path d="M27.277 46a3.34 3.34 0 0 1-2.367-.98L14 34.11l4.733-4.733 8.356 8.356L43.999 18l5.084 4.354L29.82 44.832a3.366 3.366 0 0 1-2.415 1.165c-.043.003-.086.003-.128.003" fill="#FFF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
@@ -82,9 +82,17 @@
|
|||||||
{{ $t('hall') }}
|
{{ $t('hall') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="user">
|
||||||
<a
|
<a
|
||||||
:href="bugReportMailto"
|
@click.prevent="openBugReportModal()"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ $t('reportBug') }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li v-else>
|
||||||
|
<a
|
||||||
|
href="mailto:admin@habitica.com?subject=Habitica Web Bug Report"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{{ $t('reportBug') }}
|
{{ $t('reportBug') }}
|
||||||
|
|||||||
43
website/client/src/components/bug-report-modal.stories.js
Normal file
43
website/client/src/components/bug-report-modal.stories.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import { storiesOf } from '@storybook/vue';
|
||||||
|
import { withKnobs } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
|
||||||
|
import bugReportModal from '@/components/bugReportModal';
|
||||||
|
import bugReportSuccessModal from '@/components/bugReportSuccessModal';
|
||||||
|
|
||||||
|
const stories = storiesOf('Bug Report Modal', module);
|
||||||
|
|
||||||
|
stories.addDecorator(withKnobs);
|
||||||
|
|
||||||
|
stories
|
||||||
|
.add('bugReportModal', () => ({
|
||||||
|
components: { bugReportModal },
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<bug-report-modal></bug-report-modal>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mounted () {
|
||||||
|
this.$root.$emit('bv::show::modal', 'bug-report-modal');
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.add('bugReportSuccessModal', () => ({
|
||||||
|
components: { bugReportSuccessModal },
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<bug-report-success-modal></bug-report-success-modal>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mounted () {
|
||||||
|
this.$root.$emit('bv::show::modal', 'bug-report-success-modal');
|
||||||
|
},
|
||||||
|
}));
|
||||||
243
website/client/src/components/bugReportModal.vue
Normal file
243
website/client/src/components/bugReportModal.vue
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
<template>
|
||||||
|
<b-modal
|
||||||
|
:id="modalId"
|
||||||
|
size="md"
|
||||||
|
:hide-footer="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="modal-header"
|
||||||
|
class="bug-report-modal-header"
|
||||||
|
>
|
||||||
|
<h2 v-once>
|
||||||
|
{{ $t('reportBug') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div v-once class="report-bug-header-describe">
|
||||||
|
{{ $t('reportBugHeaderDescribe') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-close">
|
||||||
|
<close-icon @click="close()" :purple="true"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
class="form"
|
||||||
|
@submit.prevent.stop="sendBugReport()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-once
|
||||||
|
for="emailInput"
|
||||||
|
>
|
||||||
|
{{ $t('email') }}
|
||||||
|
</label>
|
||||||
|
<div class="mb-2 description-label" v-once>
|
||||||
|
{{ $t('reportEmailText') }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="emailInput"
|
||||||
|
v-model="email"
|
||||||
|
class="form-control"
|
||||||
|
type="email"
|
||||||
|
:required="true"
|
||||||
|
:placeholder="$t('reportEmailPlaceholder')"
|
||||||
|
:class="{'input-invalid': emailInvalid, 'input-valid': emailValid}"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="error-label mt-2" v-if="emailInvalid">
|
||||||
|
{{ $t('reportEmailError') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label v-once>
|
||||||
|
{{ $t('reportDescription') }}
|
||||||
|
</label>
|
||||||
|
<div class="mb-2 description-label" v-once>
|
||||||
|
{{ $t('reportDescriptionText') }}
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="message"
|
||||||
|
class="form-control"
|
||||||
|
rows="5"
|
||||||
|
:required="true"
|
||||||
|
:placeholder="$t('reportDescriptionPlaceholder')"
|
||||||
|
:class="{'input-invalid': messageInvalid && this.message.length === 0}"
|
||||||
|
>
|
||||||
|
|
||||||
|
</textarea>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-primary submit-button btn-block mx-auto mt-4"
|
||||||
|
type="submit"
|
||||||
|
:disabled="!message || !emailValid"
|
||||||
|
>
|
||||||
|
{{ $t('submitBugReport') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a
|
||||||
|
class="cancel-link mx-auto mb-4"
|
||||||
|
@click.prevent="close()"
|
||||||
|
>
|
||||||
|
{{ $t('cancel') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#bug-report-modal {
|
||||||
|
.modal-header {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import '~@/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bug-report-modal-header {
|
||||||
|
color: $white;
|
||||||
|
width: 100%;
|
||||||
|
padding: 2rem 3rem 1.5rem 1.5rem;
|
||||||
|
|
||||||
|
background-image: linear-gradient(288deg, #{$purple-200}, #{$purple-300});
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-bug-header-describe {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.71;
|
||||||
|
color: $purple-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.71;
|
||||||
|
color: $gray-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-link {
|
||||||
|
color: $blue-10;
|
||||||
|
line-height: 1.71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-label {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.33;
|
||||||
|
color: $maroon-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-label {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.33;
|
||||||
|
color: $gray-100;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import isEmail from 'validator/lib/isEmail';
|
||||||
|
import closeIcon from '@/components/shared/closeIcon';
|
||||||
|
import { mapState } from '@/libs/store';
|
||||||
|
import { MODALS } from '@/libs/consts';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
closeIcon,
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
message: '',
|
||||||
|
email: '',
|
||||||
|
messageInvalid: false,
|
||||||
|
modalId: MODALS.BUG_REPORT,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async sendBugReport () {
|
||||||
|
this.messageInvalid = false;
|
||||||
|
|
||||||
|
if (this.message.length === 0) {
|
||||||
|
this.messageInvalid = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post('/api/v4/bug-report', {
|
||||||
|
message: this.message,
|
||||||
|
email: this.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.message = '';
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
this.$root.$emit('bv::show::modal', MODALS.BUG_REPORT_SUCCESS);
|
||||||
|
},
|
||||||
|
close () {
|
||||||
|
this.$root.$emit('bv::hide::modal', MODALS.BUG_REPORT);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({ user: 'user.data' }),
|
||||||
|
emailValid () {
|
||||||
|
if (this.email.length <= 3) return false;
|
||||||
|
return isEmail(this.email);
|
||||||
|
},
|
||||||
|
emailInvalid () {
|
||||||
|
if (this.email.length <= 3) return false;
|
||||||
|
return !this.emailValid;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
const { user } = this;
|
||||||
|
|
||||||
|
let email = user.auth?.local?.email;
|
||||||
|
|
||||||
|
if (!email && user.auth?.facebook?.emails) {
|
||||||
|
email = user.auth.facebook.emails?.[0]?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email && user.auth?.google?.emails) {
|
||||||
|
email = user.auth.google.emails?.[0]?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email && user.auth?.apple?.emails) {
|
||||||
|
email = user.auth.apple.emails?.[0]?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.email = email;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
154
website/client/src/components/bugReportSuccessModal.vue
Normal file
154
website/client/src/components/bugReportSuccessModal.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<b-modal
|
||||||
|
:id="modalId"
|
||||||
|
size="md"
|
||||||
|
:hide-footer="true"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="modal-header"
|
||||||
|
class="bug-report-modal-header"
|
||||||
|
>
|
||||||
|
<div class="green-bar"></div>
|
||||||
|
|
||||||
|
<div class="sub-header position-relative">
|
||||||
|
<div class="dialog-close">
|
||||||
|
<close-icon @click="close()" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="svg-icon check-icon"
|
||||||
|
v-html="icons.checkCircleIcon"
|
||||||
|
></span>
|
||||||
|
|
||||||
|
<div class="title" v-once>
|
||||||
|
{{ $t('reportSent') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text mt-3 mb-4" v-once>
|
||||||
|
{{ $t('reportSentDescription') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#bug-report-success-modal {
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
width: 330px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import '~@/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bug-report-modal-header {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.green-bar {
|
||||||
|
height: 8px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: $green-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: -0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-bug-header-describe {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.71;
|
||||||
|
color: $purple-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.71;
|
||||||
|
color: $gray-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-link {
|
||||||
|
color: $blue-10;
|
||||||
|
line-height: 1.71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-label {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.33;
|
||||||
|
color: $maroon-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-label {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.33;
|
||||||
|
color: $gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import closeIcon from '@/components/shared/closeIcon';
|
||||||
|
import checkCircleIcon from '@/assets/svg/check_circle.svg';
|
||||||
|
import { MODALS } from '@/libs/consts';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
closeIcon,
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
icons: Object.freeze({
|
||||||
|
checkCircleIcon,
|
||||||
|
}),
|
||||||
|
modalId: MODALS.BUG_REPORT_SUCCESS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close () {
|
||||||
|
this.$root.$emit('bv::hide::modal', MODALS.BUG_REPORT_SUCCESS);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
mounted () {},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -311,6 +311,7 @@ import bronzeGuildBadgeIcon from '@/assets/svg/bronze-guild-badge-small.svg';
|
|||||||
import QuestDetailModal from './questDetailModal';
|
import QuestDetailModal from './questDetailModal';
|
||||||
import RightSidebar from '@/components/groups/rightSidebar';
|
import RightSidebar from '@/components/groups/rightSidebar';
|
||||||
import InvitationListModal from './invitationListModal';
|
import InvitationListModal from './invitationListModal';
|
||||||
|
import { PAGES } from '@/libs/consts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -554,7 +555,7 @@ export default {
|
|||||||
this.$root.$emit('bv::show::modal', 'group-gems-modal');
|
this.$root.$emit('bv::show::modal', 'group-gems-modal');
|
||||||
},
|
},
|
||||||
messageLeader () {
|
messageLeader () {
|
||||||
window.open(`/private-messages?uuid=${this.group.leader.id}`);
|
window.open(`${PAGES.PRIVATE_MESSAGES}?uuid=${this.group.leader.id}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -389,6 +389,7 @@ import messageIcon from '@/assets/members/message.svg';
|
|||||||
import starIcon from '@/assets/members/star.svg';
|
import starIcon from '@/assets/members/star.svg';
|
||||||
import dots from '@/assets/svg/dots.svg';
|
import dots from '@/assets/svg/dots.svg';
|
||||||
import SelectList from '@/components/ui/selectList';
|
import SelectList from '@/components/ui/selectList';
|
||||||
|
import { PAGES } from '@/libs/consts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -558,7 +559,7 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.$root.$emit('bv::hide::modal', 'members-modal');
|
this.$root.$emit('bv::hide::modal', 'members-modal');
|
||||||
this.$router.push('/private-messages');
|
this.$router.push(PAGES.PRIVATE_MESSAGES);
|
||||||
},
|
},
|
||||||
async searchMembers (searchTerm = '') {
|
async searchMembers (searchTerm = '') {
|
||||||
this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({
|
this.members = await this.$store.state.memberModalOptions.fetchMoreMembers({
|
||||||
|
|||||||
@@ -361,7 +361,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
:href="bugReportMailto"
|
@click.prevent="openBugReportModal()"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{{ $t('reportBug') }}
|
{{ $t('reportBug') }}
|
||||||
|
|||||||
@@ -311,7 +311,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
<a
|
<a
|
||||||
class="topbar-dropdown-item dropdown-item"
|
class="topbar-dropdown-item dropdown-item"
|
||||||
:href="bugReportMailto"
|
@click.prevent="openBugReportModal()"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
{{ $t('reportBug') }}
|
{{ $t('reportBug') }}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BaseNotification from './base';
|
import BaseNotification from './base';
|
||||||
|
import { PAGES } from '@/libs/consts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -25,7 +26,7 @@ export default {
|
|||||||
props: ['notification', 'canRemove'],
|
props: ['notification', 'canRemove'],
|
||||||
methods: {
|
methods: {
|
||||||
action () {
|
action () {
|
||||||
this.$router.push('/private-messages');
|
this.$router.push(PAGES.PRIVATE_MESSAGES);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ import userIcon from '@/assets/svg/user.svg';
|
|||||||
import MenuDropdown from '../ui/customMenuDropdown';
|
import MenuDropdown from '../ui/customMenuDropdown';
|
||||||
import MessageCount from './messageCount';
|
import MessageCount from './messageCount';
|
||||||
import { EVENTS } from '@/libs/events';
|
import { EVENTS } from '@/libs/events';
|
||||||
|
import { PAGES } from '@/libs/consts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -170,7 +171,7 @@ export default {
|
|||||||
if (this.$router.history.current.name === 'privateMessages') {
|
if (this.$router.history.current.name === 'privateMessages') {
|
||||||
this.$root.$emit(EVENTS.PM_REFRESH);
|
this.$root.$emit(EVENTS.PM_REFRESH);
|
||||||
} else {
|
} else {
|
||||||
this.$router.push('/private-messages');
|
this.$router.push(PAGES.PRIVATE_MESSAGES);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showProfile (startingPage) {
|
showProfile (startingPage) {
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
<button
|
<button
|
||||||
title="close dialog"
|
title="close dialog"
|
||||||
@click="$emit('click', $event)"
|
@click="$emit('click', $event)"
|
||||||
|
:style="{
|
||||||
|
'--icon-color': iconColor,
|
||||||
|
'--icon-color-hover': iconColorHover,
|
||||||
|
}"
|
||||||
|
:class="{'purple': purple}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-once
|
v-once
|
||||||
@@ -16,6 +21,7 @@ import svgClose from '@/assets/svg/close.svg';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CloseIcon',
|
name: 'CloseIcon',
|
||||||
|
props: ['iconColor', 'iconColorHover', 'purple'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
icons: Object.freeze({
|
icons: Object.freeze({
|
||||||
@@ -43,13 +49,18 @@ export default {
|
|||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
|
||||||
|
&.purple {
|
||||||
|
--icon-color: #{$purple-400};
|
||||||
|
--icon-color-hover: #{$purple-500};
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep svg path {
|
::v-deep svg path {
|
||||||
stroke: $gray-200;
|
stroke: var(--icon-color, #{$gray-200});
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
::v-deep svg path {
|
::v-deep svg path {
|
||||||
stroke: $gray-100;
|
stroke: var(--icon-color-hover, #{$gray-100});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,11 @@
|
|||||||
<a href="mailto:admin@habitica.com">admin@habitica.com</a>
|
<a href="mailto:admin@habitica.com">admin@habitica.com</a>
|
||||||
<span v-if="user">
|
<span v-if="user">
|
||||||
<br>
|
<br>
|
||||||
{{ $t('reportBug') }}
|
|
||||||
:
|
|
||||||
<a
|
<a
|
||||||
:href="bugReportMailto"
|
@click.prevent="openBugReportModal()"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
admin@habitica.com
|
{{ $t('reportBug') }}
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
{{ $t('reportCommunityIssues') }}
|
{{ $t('reportCommunityIssues') }}
|
||||||
|
|||||||
8
website/client/src/libs/consts.js
Normal file
8
website/client/src/libs/consts.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const PAGES = {
|
||||||
|
PRIVATE_MESSAGES: '/private-messages',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MODALS = {
|
||||||
|
BUG_REPORT: 'bug-report-modal',
|
||||||
|
BUG_REPORT_SUCCESS: 'bug-report-success-modal',
|
||||||
|
};
|
||||||
@@ -1,34 +1,9 @@
|
|||||||
import { mapState } from '@/libs/store';
|
import { MODALS } from '@/libs/consts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
methods: {
|
||||||
...mapState({ user: 'user.data' }),
|
openBugReportModal () {
|
||||||
bugReportMailto () {
|
this.$root.$emit('bv::show::modal', MODALS.BUG_REPORT);
|
||||||
let subscriptionInfo = 'Not Subscribed';
|
|
||||||
if (!this.user) {
|
|
||||||
return 'mailto:admin@habitica.com?subject=Habitica Web Bug Report';
|
|
||||||
}
|
|
||||||
if (this.user.purchased.plan.customerId) {
|
|
||||||
subscriptionInfo = `
|
|
||||||
Subscription: ${this.user.purchased.plan.planId}%0d%0a
|
|
||||||
Payment Platform: ${this.user.purchased.plan.paymentMethod}%0d%0a
|
|
||||||
Customer ID: ${this.user.purchased.plan.customerId}%0d%0a
|
|
||||||
Consecutive Months: ${this.user.purchased.plan.consecutive.count}%0d%0a
|
|
||||||
Offset Months: ${this.user.purchased.plan.consecutive.offset}%0d%0a
|
|
||||||
Mystic Hourglasses: ${this.user.purchased.plan.consecutive.trinkets}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return `mailto:admin@habitica.com?subject=Habitica Web Bug Report&body=
|
|
||||||
Please describe the issue you encountered:%0d%0a%0d%0a
|
|
||||||
User ID: ${this.user._id}%0d%0a
|
|
||||||
Level: ${this.user.stats.lvl}%0d%0a
|
|
||||||
Class: ${this.user.stats.class}%0d%0a
|
|
||||||
Dailies Paused: ${this.user.preferences.sleep}%0d%0a
|
|
||||||
Uses Costume: ${this.user.preferences.costume}%0d%0a
|
|
||||||
Custom Day Start: ${this.user.preferences.dayStart}%0d%0a
|
|
||||||
Timezone Offset: ${this.user.preferences.timezoneOffset}%0d%0a
|
|
||||||
${subscriptionInfo}
|
|
||||||
`;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import getStore from '@/store';
|
|||||||
import handleRedirect from './handleRedirect';
|
import handleRedirect from './handleRedirect';
|
||||||
|
|
||||||
import ParentPage from '@/components/parentPage';
|
import ParentPage from '@/components/parentPage';
|
||||||
|
import { PAGES } from '@/libs/consts';
|
||||||
|
|
||||||
// NOTE: when adding a page make sure to implement setTitle
|
// NOTE: when adding a page make sure to implement setTitle
|
||||||
|
|
||||||
@@ -201,7 +202,7 @@ const router = new VueRouter({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: '/private-messages', name: 'privateMessages', component: MessagesIndex },
|
{ path: PAGES.PRIVATE_MESSAGES, name: 'privateMessages', component: MessagesIndex },
|
||||||
{
|
{
|
||||||
name: 'challenges',
|
name: 'challenges',
|
||||||
path: '/challenges',
|
path: '/challenges',
|
||||||
|
|||||||
@@ -93,6 +93,16 @@
|
|||||||
"audioTheme_pizildenTheme": "Pizilden's Theme",
|
"audioTheme_pizildenTheme": "Pizilden's Theme",
|
||||||
"audioTheme_farvoidTheme": "Farvoid Theme",
|
"audioTheme_farvoidTheme": "Farvoid Theme",
|
||||||
"reportBug": "Report a Bug",
|
"reportBug": "Report a Bug",
|
||||||
|
"reportBugHeaderDescribe": "Please describe the bug you’re experiencing and our team will get back to you.",
|
||||||
|
"reportEmailText": "This will only be used to contact you regarding the bug report.",
|
||||||
|
"reportEmailPlaceholder": "Your email address",
|
||||||
|
"reportEmailError": "Please provide a valid email address",
|
||||||
|
"reportDescription": "Description",
|
||||||
|
"reportDescriptionText": "Include screenshots or Javascript console errors if helpful.",
|
||||||
|
"reportDescriptionPlaceholder": "Describe the bug in detail here",
|
||||||
|
"submitBugReport": "Submit Bug Report",
|
||||||
|
"reportSent": "Bug report sent!",
|
||||||
|
"reportSentDescription": "We’ll get back to you once our team has a chance to investigate. Thank you for reporting the issue.",
|
||||||
"overview": "Overview for New Users",
|
"overview": "Overview for New Users",
|
||||||
"dateFormat": "Date Format",
|
"dateFormat": "Date Format",
|
||||||
"achievementStressbeast": "Savior of Stoïkalm",
|
"achievementStressbeast": "Savior of Stoïkalm",
|
||||||
@@ -201,5 +211,6 @@
|
|||||||
"howManyToBuy": "How many would you like to buy?",
|
"howManyToBuy": "How many would you like to buy?",
|
||||||
"contactForm": "Contact the Moderation Team",
|
"contactForm": "Contact the Moderation Team",
|
||||||
"loadEarlierMessages": "Load Earlier Messages",
|
"loadEarlierMessages": "Load Earlier Messages",
|
||||||
"askQuestion": "Ask a Question"
|
"askQuestion": "Ask a Question",
|
||||||
|
"emptyReportBugMessage": "Report Bug Message missing"
|
||||||
}
|
}
|
||||||
|
|||||||
53
website/server/controllers/api-v4/bug-report.js
Normal file
53
website/server/controllers/api-v4/bug-report.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
|
import { bugReportLogic } from '../../libs/bug-report';
|
||||||
|
|
||||||
|
const api = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v4/bug-report Report an issue
|
||||||
|
* @apiName BugReport
|
||||||
|
* @apiGroup BugReport
|
||||||
|
* @apiDescription This POST method is used to send bug reports from the Website.
|
||||||
|
* Since it needs the Users Data, it requires authentication.
|
||||||
|
*
|
||||||
|
* @apiParam (Body) {String} message Bug Report Message to sent
|
||||||
|
* @apiParam (Body) {String} email User Email
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data Result of this bug report
|
||||||
|
* @apiSuccess {Boolean} data.ok Status of this report
|
||||||
|
* @apiSuccess {String} data.message Status of this report
|
||||||
|
*
|
||||||
|
* @apiError (400) {BadRequest} emptyReportBugMessage The report message is missing.
|
||||||
|
* @apiUse UserNotFound
|
||||||
|
*/
|
||||||
|
api.bugReport = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/bug-report',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
async handler (req, res) {
|
||||||
|
req.checkBody('message', res.t('emptyReportBugMessage')).notEmpty();
|
||||||
|
req.checkBody('email', res.t('missingEmail')).notEmpty();
|
||||||
|
req.checkBody('email', res.t('notAnEmail')).isEmail();
|
||||||
|
|
||||||
|
const validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
const { message, email } = req.body;
|
||||||
|
const { user } = res.locals;
|
||||||
|
const BROWSER_UA = req.get('User-Agent');
|
||||||
|
|
||||||
|
const {
|
||||||
|
emailData, sendMailResult,
|
||||||
|
} = bugReportLogic(
|
||||||
|
user, email, message, BROWSER_UA,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
ok: true,
|
||||||
|
emailData,
|
||||||
|
sendMailResult,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
39
website/server/libs/bug-report.js
Normal file
39
website/server/libs/bug-report.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import nconf from 'nconf';
|
||||||
|
import { convertVariableObjectToArray, sendTxn } from './email';
|
||||||
|
|
||||||
|
export async function bugReportLogic (
|
||||||
|
user, userEmail, message, BROWSER_UA,
|
||||||
|
) {
|
||||||
|
const emailData = {
|
||||||
|
USER_ID: user._id,
|
||||||
|
USER_EMAIL: userEmail,
|
||||||
|
USER_USERNAME: user.auth.local.username,
|
||||||
|
USER_LEVEL: user.stats.lvl,
|
||||||
|
USER_CLASS: user.stats.class,
|
||||||
|
USER_DAILIES_PAUSED: user.preferences.sleep === 1 ? 'true' : 'false',
|
||||||
|
USER_COSTUME: user.preferences.costume === 1 ? 'true' : 'false',
|
||||||
|
USER_CUSTOM_DAY: user.preferences.dayStart,
|
||||||
|
USER_TIMEZONE_OFFSET: user.preferences.timezoneOffset,
|
||||||
|
USER_SUBSCRIPTION: user.purchased.plan.planId,
|
||||||
|
USER_PAYMENT_PLATFORM: user.purchased.plan.paymentMethod,
|
||||||
|
USER_CUSTOMER_ID: user.purchased.plan.customerId,
|
||||||
|
USER_CONSECUTIVE_MONTHS: user.purchased.plan.consecutive.count,
|
||||||
|
USER_OFFSET_MONTHS: user.purchased.plan.consecutive.offset,
|
||||||
|
USER_HOURGLASSES: user.purchased.plan.consecutive.trinkets,
|
||||||
|
REPORT_MSG: message,
|
||||||
|
BROWSER_UA,
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminMail = { email: nconf.get('ADMIN_EMAIL') };
|
||||||
|
|
||||||
|
const sendMailResult = await sendTxn(
|
||||||
|
adminMail,
|
||||||
|
'report-a-bug',
|
||||||
|
convertVariableObjectToArray(emailData),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendMailResult,
|
||||||
|
emailData,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -67,12 +67,32 @@ export function getGroupUrl (group) {
|
|||||||
|
|
||||||
// Send a transactional email using Mandrill through the external email server
|
// Send a transactional email using Mandrill through the external email server
|
||||||
export async function sendTxn (mailingInfoArray, emailType, variables, personalVariables) {
|
export async function sendTxn (mailingInfoArray, emailType, variables, personalVariables) {
|
||||||
mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray]; // eslint-disable-line no-param-reassign, max-len
|
if (!Array.isArray(mailingInfoArray)) {
|
||||||
|
mailingInfoArray = [mailingInfoArray]; // eslint-disable-line no-param-reassign
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of mailingInfoArray) {
|
||||||
|
if (typeof entry === 'string'
|
||||||
|
&& (typeof entry._id === 'undefined' && typeof entry.email === 'undefined')
|
||||||
|
) {
|
||||||
|
throw new Error('Argument Error mailingInfoArray: does not contain email or _id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variables && !Array.isArray(variables)) {
|
||||||
|
throw new Error('Argument Error variables: is not an array');
|
||||||
|
}
|
||||||
|
|
||||||
variables = [ // eslint-disable-line no-param-reassign
|
variables = [ // eslint-disable-line no-param-reassign
|
||||||
{ name: 'BASE_URL', content: BASE_URL },
|
{ name: 'BASE_URL', content: BASE_URL },
|
||||||
].concat(variables || []);
|
].concat(variables || []);
|
||||||
|
|
||||||
|
for (const variable of variables) {
|
||||||
|
if (typeof variable.name === 'undefined' && typeof variable.content === 'undefined') {
|
||||||
|
throw new Error('Argument Error variables: does not contain name or content');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// It's important to pass at least a user with its `preferences`
|
// It's important to pass at least a user with its `preferences`
|
||||||
// as we need to check if he unsubscribed
|
// as we need to check if he unsubscribed
|
||||||
mailingInfoArray = mailingInfoArray // eslint-disable-line no-param-reassign
|
mailingInfoArray = mailingInfoArray // eslint-disable-line no-param-reassign
|
||||||
@@ -157,3 +177,17 @@ export async function sendTxn (mailingInfoArray, emailType, variables, personalV
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertVariableObjectToArray (variableObject) {
|
||||||
|
const variablesArray = [];
|
||||||
|
|
||||||
|
const objectKeys = Object.keys(variableObject);
|
||||||
|
for (const propName of objectKeys) {
|
||||||
|
variablesArray.push({
|
||||||
|
name: propName,
|
||||||
|
content: variableObject[propName],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return variablesArray;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user