Merge branch 'release/1.13.1'
This commit is contained in:
@@ -208,7 +208,7 @@ export default {
|
|||||||
max-width: 32rem;
|
max-width: 32rem;
|
||||||
padding: var(--space-small) var(--space-normal);
|
padding: var(--space-small) var(--space-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-private .file.message-text__wrap {
|
&.is-private .file.message-text__wrap {
|
||||||
.ion-document-text {
|
.ion-document-text {
|
||||||
color: var(--w-400);
|
color: var(--w-400);
|
||||||
@@ -220,10 +220,18 @@ export default {
|
|||||||
color: var(--w-400);
|
color: var(--w-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-private.is-text > .message-text__wrap .link {
|
&.is-private.is-text > .message-text__wrap .link {
|
||||||
color: var(--w-700);
|
color: var(--w-700);
|
||||||
}
|
}
|
||||||
|
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
|
||||||
|
font-weight: var(--font-weight-black);
|
||||||
|
background: none;
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
padding: 0;
|
||||||
|
color: var(--color-body);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-pending {
|
&.is-pending {
|
||||||
|
|||||||
@@ -43,7 +43,9 @@
|
|||||||
"EDIT_CONTACT": {
|
"EDIT_CONTACT": {
|
||||||
"BUTTON_LABEL": "Edit Contact",
|
"BUTTON_LABEL": "Edit Contact",
|
||||||
"TITLE": "Edit contact",
|
"TITLE": "Edit contact",
|
||||||
"DESC": "Edit contact details",
|
"DESC": "Edit contact details"
|
||||||
|
},
|
||||||
|
"CONTACT_FORM": {
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"SUBMIT": "Submit",
|
"SUBMIT": "Submit",
|
||||||
"CANCEL": "Cancel",
|
"CANCEL": "Cancel",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div v-if="browser.browser_name" class="conversation--details">
|
<div v-if="browser.browser_name" class="conversation--details">
|
||||||
<contact-details-item
|
<contact-details-item
|
||||||
v-if="location"
|
v-if="location"
|
||||||
:title="$t('EDIT_CONTACT.FORM.LOCATION.LABEL')"
|
:title="$t('CONTACT_FORM.FORM.LOCATION.LABEL')"
|
||||||
:value="location"
|
:value="location"
|
||||||
icon="ion-map"
|
icon="ion-map"
|
||||||
emoji="📍"
|
emoji="📍"
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<form class="contact--form" @submit.prevent="handleSubmit">
|
||||||
|
<div class="row">
|
||||||
|
<div class="medium-9 columns">
|
||||||
|
<label :class="{ error: $v.name.$error }">
|
||||||
|
{{ $t('CONTACT_FORM.FORM.NAME.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="name"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('CONTACT_FORM.FORM.NAME.PLACEHOLDER')"
|
||||||
|
@input="$v.name.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label :class="{ error: $v.email.$error }">
|
||||||
|
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.LABEL') }}
|
||||||
|
<input
|
||||||
|
v-model.trim="email"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.PLACEHOLDER')"
|
||||||
|
@input="$v.email.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<label :class="{ error: $v.description.$error }">
|
||||||
|
{{ $t('CONTACT_FORM.FORM.BIO.LABEL') }}
|
||||||
|
<textarea
|
||||||
|
v-model.trim="description"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('CONTACT_FORM.FORM.BIO.PLACEHOLDER')"
|
||||||
|
@input="$v.description.$touch"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="phoneNumber"
|
||||||
|
class="medium-6 columns"
|
||||||
|
:label="$t('CONTACT_FORM.FORM.PHONE_NUMBER.LABEL')"
|
||||||
|
:placeholder="$t('CONTACT_FORM.FORM.PHONE_NUMBER.PLACEHOLDER')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<woot-input
|
||||||
|
v-model.trim="companyName"
|
||||||
|
class="medium-6 columns"
|
||||||
|
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
|
||||||
|
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
|
||||||
|
/>
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<label>
|
||||||
|
Social Profiles
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
v-for="socialProfile in socialProfileKeys"
|
||||||
|
:key="socialProfile.key"
|
||||||
|
class="input-group"
|
||||||
|
>
|
||||||
|
<span class="input-group-label">{{ socialProfile.prefixURL }}</span>
|
||||||
|
<input
|
||||||
|
v-model="socialProfileUserNames[socialProfile.key]"
|
||||||
|
class="input-group-field"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<woot-submit-button
|
||||||
|
:loading="inProgress"
|
||||||
|
:button-text="$t('CONTACT_FORM.FORM.SUBMIT')"
|
||||||
|
/>
|
||||||
|
<button class="button clear" @click.prevent="onCancel">
|
||||||
|
{{ $t('CONTACT_FORM.FORM.CANCEL') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { DuplicateContactException } from 'shared/helpers/CustomErrors';
|
||||||
|
import { required } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
contact: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
inProgress: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
onSubmit: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hasADuplicateContact: false,
|
||||||
|
duplicateContact: {},
|
||||||
|
companyName: '',
|
||||||
|
description: '',
|
||||||
|
email: '',
|
||||||
|
name: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
socialProfileUserNames: {
|
||||||
|
facebook: '',
|
||||||
|
twitter: '',
|
||||||
|
linkedin: '',
|
||||||
|
},
|
||||||
|
socialProfileKeys: [
|
||||||
|
{ key: 'facebook', prefixURL: 'https://facebook.com/' },
|
||||||
|
{ key: 'twitter', prefixURL: 'https://twitter.com/' },
|
||||||
|
{ key: 'linkedin', prefixURL: 'https://linkedin.com/' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
name: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
description: {},
|
||||||
|
email: {},
|
||||||
|
companyName: {},
|
||||||
|
phoneNumber: {},
|
||||||
|
bio: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
contact() {
|
||||||
|
this.setContactObject();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.setContactObject();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onCancel() {
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
setContactObject() {
|
||||||
|
const { email: email, phone_number: phoneNumber, name } = this.contact;
|
||||||
|
const additionalAttributes = this.contact.additional_attributes || {};
|
||||||
|
|
||||||
|
this.name = name || '';
|
||||||
|
this.email = email || '';
|
||||||
|
this.phoneNumber = phoneNumber || '';
|
||||||
|
this.companyName = additionalAttributes.company_name || '';
|
||||||
|
this.description = additionalAttributes.description || '';
|
||||||
|
const {
|
||||||
|
social_profiles: socialProfiles = {},
|
||||||
|
screen_name: twitterScreenName,
|
||||||
|
} = additionalAttributes;
|
||||||
|
this.socialProfileUserNames = {
|
||||||
|
twitter: socialProfiles.twitter || twitterScreenName || '',
|
||||||
|
facebook: socialProfiles.facebook || '',
|
||||||
|
linkedin: socialProfiles.linkedin || '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getContactObject() {
|
||||||
|
return {
|
||||||
|
id: this.contact.id,
|
||||||
|
name: this.name,
|
||||||
|
email: this.email,
|
||||||
|
phone_number: this.phoneNumber,
|
||||||
|
additional_attributes: {
|
||||||
|
...this.contact.additional_attributes,
|
||||||
|
description: this.description,
|
||||||
|
company_name: this.companyName,
|
||||||
|
social_profiles: this.socialProfileUserNames,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
resetDuplicate() {
|
||||||
|
this.hasADuplicateContact = false;
|
||||||
|
this.duplicateContact = {};
|
||||||
|
},
|
||||||
|
async handleSubmit() {
|
||||||
|
this.resetDuplicate();
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.onSubmit(this.getContactObject());
|
||||||
|
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof DuplicateContactException) {
|
||||||
|
this.hasADuplicateContact = true;
|
||||||
|
this.duplicateContact = error.data;
|
||||||
|
this.showAlert(this.$t('CONTACT_FORM.CONTACT_ALREADY_EXIST'));
|
||||||
|
} else {
|
||||||
|
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.contact--form {
|
||||||
|
padding: var(--space-normal) var(--space-large) var(--space-large);
|
||||||
|
|
||||||
|
.columns {
|
||||||
|
padding: 0 var(--space-smaller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,96 +7,23 @@
|
|||||||
"
|
"
|
||||||
:header-content="$t('EDIT_CONTACT.DESC')"
|
:header-content="$t('EDIT_CONTACT.DESC')"
|
||||||
/>
|
/>
|
||||||
<form class="edit-contact--form" @submit.prevent="onSubmit">
|
<contact-form
|
||||||
<div class="row">
|
:contact="contact"
|
||||||
<div class="medium-9 columns">
|
:in-progress="uiFlags.isUpdating"
|
||||||
<label :class="{ error: $v.name.$error }">
|
:on-submit="onSubmit"
|
||||||
{{ $t('EDIT_CONTACT.FORM.NAME.LABEL') }}
|
/>
|
||||||
<input
|
|
||||||
v-model.trim="name"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('EDIT_CONTACT.FORM.NAME.PLACEHOLDER')"
|
|
||||||
@input="$v.name.$touch"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label :class="{ error: $v.email.$error }">
|
|
||||||
{{ $t('EDIT_CONTACT.FORM.EMAIL_ADDRESS.LABEL') }}
|
|
||||||
<input
|
|
||||||
v-model.trim="email"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('EDIT_CONTACT.FORM.EMAIL_ADDRESS.PLACEHOLDER')"
|
|
||||||
@input="$v.email.$touch"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<label :class="{ error: $v.description.$error }">
|
|
||||||
{{ $t('EDIT_CONTACT.FORM.BIO.LABEL') }}
|
|
||||||
<textarea
|
|
||||||
v-model.trim="description"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('EDIT_CONTACT.FORM.BIO.PLACEHOLDER')"
|
|
||||||
@input="$v.description.$touch"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<woot-input
|
|
||||||
v-model.trim="phoneNumber"
|
|
||||||
class="medium-6 columns"
|
|
||||||
:label="$t('EDIT_CONTACT.FORM.PHONE_NUMBER.LABEL')"
|
|
||||||
:placeholder="$t('EDIT_CONTACT.FORM.PHONE_NUMBER.PLACEHOLDER')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<woot-input
|
|
||||||
v-model.trim="companyName"
|
|
||||||
class="medium-6 columns"
|
|
||||||
:label="$t('EDIT_CONTACT.FORM.COMPANY_NAME.LABEL')"
|
|
||||||
:placeholder="$t('EDIT_CONTACT.FORM.COMPANY_NAME.PLACEHOLDER')"
|
|
||||||
/>
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<label>
|
|
||||||
Social Profiles
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
v-for="socialProfile in socialProfileKeys"
|
|
||||||
:key="socialProfile.key"
|
|
||||||
class="input-group"
|
|
||||||
>
|
|
||||||
<span class="input-group-label">{{ socialProfile.prefixURL }}</span>
|
|
||||||
<input
|
|
||||||
v-model="socialProfileUserNames[socialProfile.key]"
|
|
||||||
class="input-group-field"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
<woot-submit-button
|
|
||||||
:loading="uiFlags.isUpdating"
|
|
||||||
:button-text="$t('EDIT_CONTACT.FORM.SUBMIT')"
|
|
||||||
/>
|
|
||||||
<button class="button clear" @click.prevent="onCancel">
|
|
||||||
{{ $t('EDIT_CONTACT.FORM.CANCEL') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</woot-modal>
|
</woot-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import { DuplicateContactException } from 'shared/helpers/CustomErrors';
|
|
||||||
import { required } from 'vuelidate/lib/validators';
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import ContactForm from './ContactForm';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
components: {
|
||||||
|
ContactForm,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -107,120 +34,20 @@ export default {
|
|||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hasADuplicateContact: false,
|
|
||||||
duplicateContact: {},
|
|
||||||
companyName: '',
|
|
||||||
description: '',
|
|
||||||
email: '',
|
|
||||||
name: '',
|
|
||||||
phoneNumber: '',
|
|
||||||
socialProfileUserNames: {
|
|
||||||
facebook: '',
|
|
||||||
twitter: '',
|
|
||||||
linkedin: '',
|
|
||||||
},
|
|
||||||
socialProfileKeys: [
|
|
||||||
{ key: 'facebook', prefixURL: 'https://facebook.com/' },
|
|
||||||
{ key: 'twitter', prefixURL: 'https://twitter.com/' },
|
|
||||||
{ key: 'linkedin', prefixURL: 'https://linkedin.com/' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
validations: {
|
|
||||||
name: {
|
|
||||||
required,
|
|
||||||
},
|
|
||||||
description: {},
|
|
||||||
email: {},
|
|
||||||
companyName: {},
|
|
||||||
phoneNumber: {},
|
|
||||||
bio: {},
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
uiFlags: 'contacts/getUIFlags',
|
uiFlags: 'contacts/getUIFlags',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
contact() {
|
|
||||||
this.setContactObject();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setContactObject();
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
onCancel() {
|
onCancel() {
|
||||||
this.$emit('cancel');
|
this.$emit('cancel');
|
||||||
},
|
},
|
||||||
setContactObject() {
|
async onSubmit(contactItem) {
|
||||||
const { email: email, phone_number: phoneNumber, name } = this.contact;
|
await this.$store.dispatch('contacts/update', contactItem);
|
||||||
const additionalAttributes = this.contact.additional_attributes || {};
|
|
||||||
|
|
||||||
this.name = name || '';
|
|
||||||
this.email = email || '';
|
|
||||||
this.phoneNumber = phoneNumber || '';
|
|
||||||
this.companyName = additionalAttributes.company_name || '';
|
|
||||||
this.description = additionalAttributes.description || '';
|
|
||||||
const {
|
|
||||||
social_profiles: socialProfiles = {},
|
|
||||||
screen_name: twitterScreenName,
|
|
||||||
} = additionalAttributes;
|
|
||||||
this.socialProfileUserNames = {
|
|
||||||
twitter: socialProfiles.twitter || twitterScreenName || '',
|
|
||||||
facebook: socialProfiles.facebook || '',
|
|
||||||
linkedin: socialProfiles.linkedin || '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getContactObject() {
|
|
||||||
return {
|
|
||||||
id: this.contact.id,
|
|
||||||
name: this.name,
|
|
||||||
email: this.email,
|
|
||||||
phone_number: this.phoneNumber,
|
|
||||||
additional_attributes: {
|
|
||||||
...this.contact.additional_attributes,
|
|
||||||
description: this.description,
|
|
||||||
company_name: this.companyName,
|
|
||||||
social_profiles: this.socialProfileUserNames,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
resetDuplicate() {
|
|
||||||
this.hasADuplicateContact = false;
|
|
||||||
this.duplicateContact = {};
|
|
||||||
},
|
|
||||||
async onSubmit() {
|
|
||||||
this.resetDuplicate();
|
|
||||||
this.$v.$touch();
|
|
||||||
if (this.$v.$invalid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.$store.dispatch('contacts/update', this.getContactObject());
|
|
||||||
this.showAlert(this.$t('EDIT_CONTACT.SUCCESS_MESSAGE'));
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof DuplicateContactException) {
|
|
||||||
this.hasADuplicateContact = true;
|
|
||||||
this.duplicateContact = error.data;
|
|
||||||
this.showAlert(this.$t('EDIT_CONTACT.CONTACT_ALREADY_EXIST'));
|
|
||||||
} else {
|
|
||||||
this.showAlert(this.$t('EDIT_CONTACT.ERROR_MESSAGE'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.edit-contact--form {
|
|
||||||
padding: var(--space-normal) var(--space-large) var(--space-large);
|
|
||||||
|
|
||||||
.columns {
|
|
||||||
padding: 0 var(--space-smaller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -49,10 +49,12 @@
|
|||||||
:username="notificationItem.primary_actor.meta.assignee.name"
|
:username="notificationItem.primary_actor.meta.assignee.name"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right timestamp--column">
|
<td>
|
||||||
<span class="notification--created-at">
|
<div class="text-right timestamp--column">
|
||||||
{{ dynamicTime(notificationItem.created_at) }}
|
<span class="notification--created-at">
|
||||||
</span>
|
{{ dynamicTime(notificationItem.created_at) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
@@ -191,7 +193,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timestamp--column {
|
.timestamp--column {
|
||||||
width: 12rem;
|
min-width: 13rem;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification--message-title {
|
.notification--message-title {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class NotificationListener < BaseListener
|
|||||||
notification_type: 'assigned_conversation_new_message',
|
notification_type: 'assigned_conversation_new_message',
|
||||||
user: conversation.assignee,
|
user: conversation.assignee,
|
||||||
account: account,
|
account: account,
|
||||||
primary_actor: conversation
|
primary_actor: message
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -44,12 +44,13 @@ class Notification < ApplicationRecord
|
|||||||
PRIMARY_ACTORS = ['Conversation'].freeze
|
PRIMARY_ACTORS = ['Conversation'].freeze
|
||||||
|
|
||||||
def push_event_data
|
def push_event_data
|
||||||
|
# Secondary actor could be nil for cases like system assigning conversation
|
||||||
{
|
{
|
||||||
id: id,
|
id: id,
|
||||||
notification_type: notification_type,
|
notification_type: notification_type,
|
||||||
primary_actor_type: primary_actor_type,
|
primary_actor_type: primary_actor_type,
|
||||||
primary_actor_id: primary_actor_id,
|
primary_actor_id: primary_actor_id,
|
||||||
primary_actor: primary_actor&.push_event_data,
|
primary_actor: primary_actor.push_event_data,
|
||||||
read_at: read_at,
|
read_at: read_at,
|
||||||
secondary_actor: secondary_actor&.push_event_data,
|
secondary_actor: secondary_actor&.push_event_data,
|
||||||
user: user&.push_event_data,
|
user: user&.push_event_data,
|
||||||
@@ -59,7 +60,6 @@ class Notification < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
# TODO: move to a data presenter
|
# TODO: move to a data presenter
|
||||||
# rubocop:disable Metrics/CyclomaticComplexity
|
|
||||||
def push_message_title
|
def push_message_title
|
||||||
case notification_type
|
case notification_type
|
||||||
when 'conversation_creation'
|
when 'conversation_creation'
|
||||||
@@ -69,19 +69,18 @@ class Notification < ApplicationRecord
|
|||||||
when 'assigned_conversation_new_message'
|
when 'assigned_conversation_new_message'
|
||||||
I18n.t(
|
I18n.t(
|
||||||
'notifications.notification_title.assigned_conversation_new_message',
|
'notifications.notification_title.assigned_conversation_new_message',
|
||||||
display_id: primary_actor.display_id,
|
display_id: conversation.display_id,
|
||||||
content: primary_actor&.messages&.incoming&.last&.content
|
content: primary_actor.content.truncate_words(10)
|
||||||
)
|
)
|
||||||
when 'conversation_mention'
|
when 'conversation_mention'
|
||||||
I18n.t('notifications.notification_title.conversation_mention', display_id: primary_actor.conversation.display_id, name: secondary_actor.name)
|
I18n.t('notifications.notification_title.conversation_mention', display_id: conversation.display_id, name: secondary_actor.name)
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/CyclomaticComplexity
|
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
return primary_actor.conversation if ['conversation_mention'].include? notification_type
|
return primary_actor.conversation if %w[assigned_conversation_new_message conversation_mention].include? notification_type
|
||||||
|
|
||||||
primary_actor
|
primary_actor
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,18 +11,19 @@ json.data do
|
|||||||
json.notification_type notification.notification_type
|
json.notification_type notification.notification_type
|
||||||
json.push_message_title notification.push_message_title
|
json.push_message_title notification.push_message_title
|
||||||
# TODO: front end assumes primary actor to be conversation. should fix in future
|
# TODO: front end assumes primary actor to be conversation. should fix in future
|
||||||
if notification.notification_type == 'conversation_mention'
|
if %w[assigned_conversation_new_message conversation_mention].include? notification.notification_type
|
||||||
json.primary_actor_type 'Conversation'
|
json.primary_actor_type 'Conversation'
|
||||||
json.primary_actor_id notification.primary_actor.conversation_id
|
json.primary_actor_id notification.conversation.id
|
||||||
json.primary_actor notification.primary_actor&.conversation&.push_event_data
|
json.primary_actor notification.conversation.push_event_data
|
||||||
else
|
else
|
||||||
json.primary_actor_type notification.primary_actor_type
|
json.primary_actor_type notification.primary_actor_type
|
||||||
json.primary_actor_id notification.primary_actor_id
|
json.primary_actor_id notification.primary_actor_id
|
||||||
json.primary_actor notification.primary_actor&.push_event_data
|
json.primary_actor notification.primary_actor.push_event_data
|
||||||
end
|
end
|
||||||
json.read_at notification.read_at
|
json.read_at notification.read_at
|
||||||
|
# Secondary actor could be nil for cases like system assigning conversation
|
||||||
json.secondary_actor notification.secondary_actor&.push_event_data
|
json.secondary_actor notification.secondary_actor&.push_event_data
|
||||||
json.user notification.user&.push_event_data
|
json.user notification.user.push_event_data
|
||||||
json.created_at notification.created_at.to_i
|
json.created_at notification.created_at.to_i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
shared: &shared
|
shared: &shared
|
||||||
version: '1.13.0'
|
version: '1.13.1'
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *shared
|
<<: *shared
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class RemoveOrphanInboxMembersFromInboxes < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
Account.all.map do |account|
|
||||||
|
user_ids = account.users.all.map(&:id)
|
||||||
|
inboxes = account.inboxes
|
||||||
|
inboxes.each do |inbox|
|
||||||
|
inbox.inbox_members.each do |inbox_member|
|
||||||
|
inbox_member.destroy! unless user_ids.include?(inbox_member.user_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
5
db/migrate/20210219085719_remove_old_notifications.rb
Normal file
5
db/migrate/20210219085719_remove_old_notifications.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class RemoveOldNotifications < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
Notification.where(notification_type: 'assigned_conversation_new_message').destroy_all
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_02_12_154240) do
|
ActiveRecord::Schema.define(version: 2021_02_19_085719) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
|||||||
47
deployment/nginx_chatwoot.conf
Normal file
47
deployment/nginx_chatwoot.conf
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name chatwoot.domain.com www.chatwoot.domain.com;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/chatwoot_access_80.log;
|
||||||
|
error_log /var/log/nginx/chatwoot_error_80.log;
|
||||||
|
|
||||||
|
return 301 https://chatwoot.domain.com/;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name chatwoot.domain.com www.chatwoot.domain.com;
|
||||||
|
|
||||||
|
underscores_in_headers on;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/chatwoot_access_443.log;
|
||||||
|
error_log /var/log/nginx/chatwoot_error_443.log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass_header Authorization;
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Ssl on; # Optional
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection “”;
|
||||||
|
proxy_buffering off;
|
||||||
|
client_max_body_size 0;
|
||||||
|
proxy_read_timeout 36000s;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/chatwoot.domain.com/fullchain.pem; # managed by Certbot
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/chatwoot.domain.com/privkey.pem; # managed by Certbot
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_dhparam /etc/ssl/dhparam;
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
# Description: Chatwoot installation script
|
# Description: Chatwoot installation script
|
||||||
# OS: Ubuntu 20.04 LTS / Ubuntu 20.10
|
# OS: Ubuntu 20.04 LTS / Ubuntu 20.10
|
||||||
# Script Version: 0.2
|
# Script Version: 0.5
|
||||||
|
# Run this script as root
|
||||||
|
|
||||||
apt update && apt upgrade -y
|
apt update && apt upgrade -y
|
||||||
apt install -y curl
|
apt install -y curl
|
||||||
@@ -17,14 +18,14 @@ apt install -y \
|
|||||||
libssl-dev libyaml-dev libreadline-dev gnupg2 nginx redis-server \
|
libssl-dev libyaml-dev libreadline-dev gnupg2 nginx redis-server \
|
||||||
redis-tools postgresql postgresql-contrib certbot \
|
redis-tools postgresql postgresql-contrib certbot \
|
||||||
python3-certbot-nginx nodejs yarn patch ruby-dev zlib1g-dev liblzma-dev \
|
python3-certbot-nginx nodejs yarn patch ruby-dev zlib1g-dev liblzma-dev \
|
||||||
libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev
|
libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev nginx-full
|
||||||
|
|
||||||
adduser --disabled-login --gecos "" chatwoot
|
adduser --disabled-login --gecos "" chatwoot
|
||||||
|
|
||||||
sudo -i -u chatwoot bash << EOF
|
|
||||||
gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
||||||
|
gpg2 --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
||||||
curl -sSL https://get.rvm.io | bash -s stable
|
curl -sSL https://get.rvm.io | bash -s stable
|
||||||
EOF
|
addgroup chatwoot rvm
|
||||||
|
|
||||||
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
|
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
|
||||||
sudo -i -u postgres psql << EOF
|
sudo -i -u postgres psql << EOF
|
||||||
@@ -32,6 +33,12 @@ sudo -i -u postgres psql << EOF
|
|||||||
CREATE USER chatwoot CREATEDB;
|
CREATE USER chatwoot CREATEDB;
|
||||||
ALTER USER chatwoot PASSWORD :'pass';
|
ALTER USER chatwoot PASSWORD :'pass';
|
||||||
ALTER ROLE chatwoot SUPERUSER;
|
ALTER ROLE chatwoot SUPERUSER;
|
||||||
|
UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1';
|
||||||
|
DROP DATABASE template1;
|
||||||
|
CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE';
|
||||||
|
UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1';
|
||||||
|
\c template1
|
||||||
|
VACUUM FREEZE;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
systemctl enable redis-server.service
|
systemctl enable redis-server.service
|
||||||
@@ -49,9 +56,9 @@ rvm use 2.7.2 --default
|
|||||||
git clone https://github.com/chatwoot/chatwoot.git
|
git clone https://github.com/chatwoot/chatwoot.git
|
||||||
cd chatwoot
|
cd chatwoot
|
||||||
if [[ -z "$1" ]]; then
|
if [[ -z "$1" ]]; then
|
||||||
git checkout master;
|
git checkout master;
|
||||||
else
|
else
|
||||||
git checkout $1;
|
git checkout $1;
|
||||||
fi
|
fi
|
||||||
bundle
|
bundle
|
||||||
yarn
|
yarn
|
||||||
@@ -76,8 +83,27 @@ cp /home/chatwoot/chatwoot/deployment/chatwoot.target /etc/systemd/system/chatwo
|
|||||||
systemctl enable chatwoot.target
|
systemctl enable chatwoot.target
|
||||||
systemctl start chatwoot.target
|
systemctl start chatwoot.target
|
||||||
|
|
||||||
|
read -p 'Would you like to configure Webserver and SSL (yes or no): ' configure_webserver
|
||||||
|
|
||||||
|
if [ $configure_webserver != "yes" ]
|
||||||
|
then
|
||||||
echo "Woot! Woot!! Chatwoot server installation is complete"
|
echo "Woot! Woot!! Chatwoot server installation is complete"
|
||||||
echo "The server will be accessible at http://<server-ip>:3000"
|
echo "The server will be accessible at http://<server-ip>:3000"
|
||||||
echo "To configure a domain and SSL certificate, follow the guide at https://www.chatwoot.com/docs/deployment/deploy-chatwoot-in-linux-vm"
|
echo "To configure a domain and SSL certificate, follow the guide at https://www.chatwoot.com/docs/deployment/deploy-chatwoot-in-linux-vm"
|
||||||
|
else
|
||||||
# TODO: Auto-configure Nginx with SSL certificate
|
read -p 'What is your domain name server (chatwoot.domain.com for example) : ' domain_name
|
||||||
|
curl https://ssl-config.mozilla.org/ffdhe4096.txt >> /etc/ssl/dhparam
|
||||||
|
wget https://raw.githubusercontent.com/chatwoot/chatwoot/develop/deployment/nginx_chatwoot.conf
|
||||||
|
cp nginx_chatwoot.conf /etc/nginx/sites-available/nginx_chatwoot.conf
|
||||||
|
certbot certonly --nginx -d $domain_name
|
||||||
|
sed -i "s/chatwoot.domain.com/$domain_name/g" /etc/nginx/sites-available/nginx_chatwoot.conf
|
||||||
|
ln -s /etc/nginx/sites-available/nginx_chatwoot.conf /etc/nginx/sites-enabled/nginx_chatwoot.conf
|
||||||
|
systemctl restart nginx
|
||||||
|
sudo -i -u chatwoot << EOF
|
||||||
|
cd chatwoot
|
||||||
|
sed -i "s/http:\/\/0.0.0.0:3000/https:\/\/$domain_name/g" .env
|
||||||
|
EOF
|
||||||
|
systemctl restart chatwoot.target
|
||||||
|
echo "Woot! Woot!! Chatwoot server installation is complete"
|
||||||
|
echo "The server will be accessible at https://$domain_name"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@chatwoot/chatwoot",
|
"name": "@chatwoot/chatwoot",
|
||||||
"version": "1.13.0",
|
"version": "1.13.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"eslint": "eslint app/javascript --fix",
|
"eslint": "eslint app/javascript --fix",
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ RSpec.describe Notification do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'returns appropriate title suited for the notification type assigned_conversation_new_message' do
|
it 'returns appropriate title suited for the notification type assigned_conversation_new_message' do
|
||||||
notification = create(:notification, notification_type: 'assigned_conversation_new_message')
|
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
|
||||||
|
notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
|
||||||
|
|
||||||
expect(notification.push_message_title).to eq "[New message] - ##{notification.primary_actor.display_id} "
|
expect(notification.push_message_title).to eq "[New message] - ##{notification.conversation.display_id} \
|
||||||
|
#{message.content.truncate_words(10)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns appropriate title suited for the notification type conversation_mention' do
|
it 'returns appropriate title suited for the notification type conversation_mention' do
|
||||||
|
|||||||
Reference in New Issue
Block a user