parent
e5e73a08fe
commit
8e6ce3a813
29 changed files with 416 additions and 278 deletions
|
@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
|
|||
before_action :note, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@notes = @contact.notes.includes(:user)
|
||||
@notes = @contact.notes.latest.includes(:user)
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -2,7 +2,27 @@ import ApiClient from './ApiClient';
|
|||
|
||||
class ContactNotes extends ApiClient {
|
||||
constructor() {
|
||||
super('contact_notes', { accountScoped: true });
|
||||
super('notes', { accountScoped: true });
|
||||
this.contactId = null;
|
||||
}
|
||||
|
||||
get url() {
|
||||
return `${this.baseUrl()}/contacts/${this.contactId}/notes`;
|
||||
}
|
||||
|
||||
get(contactId) {
|
||||
this.contactId = contactId;
|
||||
return super.get();
|
||||
}
|
||||
|
||||
create(contactId, content) {
|
||||
this.contactId = contactId;
|
||||
return super.create({ content });
|
||||
}
|
||||
|
||||
delete(contactId, id) {
|
||||
this.contactId = contactId;
|
||||
return super.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
.card {
|
||||
margin-bottom: var(--space-small);
|
||||
padding: var(--space-small);
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.button-wrapper .button.link.grey-btn {
|
||||
|
|
|
@ -55,6 +55,10 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,44 @@
|
|||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.fs-small {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.fs-default {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
|
||||
.fw-medium {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.p-normal {
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.overflow-scroll {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.border-right {
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.border-left {
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
@import 'date-picker';
|
||||
@import 'utility-helpers';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
@import '~bourbon/core/bourbon';
|
||||
|
@ -50,3 +49,4 @@
|
|||
@import 'plugins/multiselect';
|
||||
@import 'plugins/dropdown';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
@import 'utility-helpers';
|
||||
|
|
|
@ -21,10 +21,6 @@
|
|||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.display-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<span class="back-button ion-ios-arrow-left" @click.capture="goBack">
|
||||
{{ $t('GENERAL_SETTINGS.BACK') }}
|
||||
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -12,6 +12,10 @@ export default {
|
|||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
|
|
|
@ -64,6 +64,7 @@ export default {
|
|||
placeholder: { type: String, default: '' },
|
||||
isPrivate: { type: Boolean, default: false },
|
||||
isFormatMode: { type: Boolean, default: false },
|
||||
enableSuggestions: { type: Boolean, default: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -78,6 +79,10 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
plugins() {
|
||||
if (!this.enableSuggestions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
suggestionsPlugin({
|
||||
matcher: triggerCharacters('@'),
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"COMPANY": "Company",
|
||||
"LOCATION": "Location",
|
||||
"CONVERSATION_TITLE": "Conversation Details",
|
||||
"VIEW_PROFILE": "View Profile",
|
||||
"BROWSER": "Browser",
|
||||
"OS": "Operating System",
|
||||
"INITIATED_FROM": "Initiated from",
|
||||
|
@ -188,6 +189,10 @@
|
|||
"VIEW_DETAILS": "View details"
|
||||
}
|
||||
},
|
||||
"CONTACT_PROFILE": {
|
||||
"BACK_BUTTON": "Contacts",
|
||||
"LOADING": "Loading contact profile..."
|
||||
},
|
||||
"REMINDER": {
|
||||
"ADD_BUTTON": {
|
||||
"BUTTON": "Add",
|
||||
|
@ -199,16 +204,21 @@
|
|||
}
|
||||
},
|
||||
"NOTES": {
|
||||
"FETCHING_NOTES": "Fetching notes...",
|
||||
"NOT_AVAILABLE": "There are no notes created for this contact",
|
||||
"HEADER": {
|
||||
"TITLE": "Notes"
|
||||
},
|
||||
"LIST": {
|
||||
"LABEL": "added a note"
|
||||
},
|
||||
"ADD": {
|
||||
"BUTTON": "Add",
|
||||
"PLACEHOLDER": "Add a note",
|
||||
"TITLE": "Shift + Enter to create a note"
|
||||
},
|
||||
"FOOTER": {
|
||||
"BUTTON": "View all notes"
|
||||
"CONTENT_HEADER": {
|
||||
"DELETE": "Delete note"
|
||||
}
|
||||
},
|
||||
"EVENTS": {
|
||||
|
|
|
@ -103,7 +103,8 @@
|
|||
},
|
||||
"APP_GLOBAL": {
|
||||
"TRIAL_MESSAGE": "days trial remaining.",
|
||||
"TRAIL_BUTTON": "Buy Now"
|
||||
"TRAIL_BUTTON": "Buy Now",
|
||||
"DELETED_USER": "Deleted User"
|
||||
},
|
||||
"COMPONENTS": {
|
||||
"CODE": {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { frontendURL } from '../../helper/URLHelper';
|
|||
const contacts = accountId => ({
|
||||
routes: [
|
||||
'contacts_dashboard',
|
||||
'contacts_dashboard_manage',
|
||||
'contact_profile_dashboard',
|
||||
'contacts_labels_dashboard',
|
||||
],
|
||||
menuItems: {
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
<template>
|
||||
<div class="wrap">
|
||||
<div class="left">
|
||||
<contact-panel v-if="!uiFlags.isFetchingItem" :contact="contact" />
|
||||
</div>
|
||||
<div class="center"></div>
|
||||
<div class="right">
|
||||
<contact-notes :contact-id="Number(contactId)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactPanel from './ContactPanel';
|
||||
import ContactNotes from 'dashboard/modules/notes/NotesOnContactPage';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactPanel,
|
||||
ContactNotes,
|
||||
},
|
||||
props: {
|
||||
contactId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'contacts/getUIFlags',
|
||||
}),
|
||||
showEmptySearchResult() {
|
||||
const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
|
||||
return hasEmptyResults;
|
||||
},
|
||||
contact() {
|
||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchContactDetails();
|
||||
},
|
||||
methods: {
|
||||
fetchContactDetails() {
|
||||
const { contactId: id } = this;
|
||||
this.$store.dispatch('contacts/show', { id });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.wrap {
|
||||
@include three-column-grid(27.2rem);
|
||||
min-height: 0;
|
||||
|
||||
background: var(--color-background-light);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.left {
|
||||
overflow: auto;
|
||||
}
|
||||
.center {
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.right {
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,14 @@
|
|||
<template>
|
||||
<note-list :notes="notes" @add="onAdd" @delete="onDelete" />
|
||||
<note-list
|
||||
:is-fetching="uiFlags.isFetching"
|
||||
:notes="notes"
|
||||
@add="onAdd"
|
||||
@delete="onDelete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NoteList from './components/NoteList';
|
||||
|
||||
export default {
|
||||
|
@ -16,6 +22,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'contactNotes/getUIFlags' }),
|
||||
notes() {
|
||||
return this.$store.getters['contactNotes/getAllNotesByContact'](
|
||||
this.contactId
|
||||
|
|
|
@ -1,45 +1,64 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
:placeholder="$t('NOTES.ADD.PLACEHOLDER')"
|
||||
<woot-message-editor
|
||||
v-model="noteContent"
|
||||
class="input--note"
|
||||
@keydown.enter.shift.exact="onAdd"
|
||||
:placeholder="$t('NOTES.ADD.PLACEHOLDER')"
|
||||
:enable-suggestions="false"
|
||||
/>
|
||||
<div class="footer">
|
||||
<woot-button
|
||||
size="tiny"
|
||||
color-scheme="warning"
|
||||
:title="$t('NOTES.ADD.TITLE')"
|
||||
:is-disabled="buttonDisabled"
|
||||
@click="onAdd"
|
||||
>
|
||||
{{ $t('NOTES.ADD.BUTTON') }}
|
||||
{{ $t('NOTES.ADD.BUTTON') }} (⌘⏎)
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||
import { hasPressedCommandAndEnter } from 'shared/helpers/KeyboardHelpers';
|
||||
export default {
|
||||
components: {
|
||||
WootMessageEditor,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
inputText: '',
|
||||
noteContent: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
buttonDisabled() {
|
||||
return this.inputText === '';
|
||||
return this.noteContent === '';
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onMetaEnter);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onMetaEnter);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onAdd() {
|
||||
if (this.inputText !== '') {
|
||||
this.$emit('add', this.inputText);
|
||||
onMetaEnter(e) {
|
||||
if (hasPressedCommandAndEnter(e)) {
|
||||
e.preventDefault();
|
||||
this.onAdd();
|
||||
}
|
||||
this.inputText = '';
|
||||
},
|
||||
onAdd() {
|
||||
if (this.noteContent !== '') {
|
||||
this.$emit('add', this.noteContent);
|
||||
}
|
||||
this.noteContent = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -47,12 +66,14 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.input--note {
|
||||
font-size: var(--font-size-mini);
|
||||
border-color: transparent;
|
||||
margin-bottom: var(--space-small);
|
||||
padding: 0;
|
||||
resize: none;
|
||||
min-height: var(--space-larger);
|
||||
&::v-deep .ProseMirror-menubar {
|
||||
padding: 0;
|
||||
margin-top: var(--space-minus-small);
|
||||
}
|
||||
|
||||
&::v-deep .ProseMirror-woot-style {
|
||||
max-height: 36rem;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
<template>
|
||||
<div class="card note-wrap">
|
||||
<p class="note__content">
|
||||
{{ note }}
|
||||
</p>
|
||||
<div class="footer">
|
||||
<div class="header">
|
||||
<div class="meta">
|
||||
<div :title="userName">
|
||||
<thumbnail :src="thumbnail" :username="userName" size="16px" />
|
||||
</div>
|
||||
<thumbnail
|
||||
:title="noteAuthorName"
|
||||
:src="noteAuthor.thumbnail"
|
||||
:username="noteAuthorName"
|
||||
size="20px"
|
||||
/>
|
||||
<div class="date-wrap">
|
||||
<span>{{ readableTime }}</span>
|
||||
<span class="fw-medium"> {{ noteAuthorName }} </span>
|
||||
<span> {{ $t('NOTES.LIST.LABEL') }} </span>
|
||||
<span class="fw-medium"> {{ readableTime }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="ion-compose"
|
||||
color-scheme="secondary"
|
||||
@click="onEdit"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="ion-trash-b"
|
||||
|
@ -29,19 +25,21 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="note__content" v-html="formatMessage(note || '')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
|
||||
mixins: [timeMixin],
|
||||
mixins: [timeMixin, messageFormatterMixin],
|
||||
|
||||
props: {
|
||||
id: {
|
||||
|
@ -52,30 +50,29 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
userName: {
|
||||
type: String,
|
||||
default: '',
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
timeStamp: {
|
||||
createdAt: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
thumbnail: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
readableTime() {
|
||||
return this.dynamicTime(this.timeStamp);
|
||||
return this.dynamicTime(this.createdAt);
|
||||
},
|
||||
noteAuthor() {
|
||||
return this.user || {};
|
||||
},
|
||||
noteAuthorName() {
|
||||
return this.noteAuthor.name || this.$t('APP_GLOBAL.DELETED_USER');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onEdit() {
|
||||
this.$emit('edit', this.id);
|
||||
},
|
||||
onDelete() {
|
||||
this.$emit('delete', this.id);
|
||||
},
|
||||
|
@ -85,24 +82,23 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.note__content {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-bottom: var(--space-smaller);
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
|
||||
.footer {
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
font-size: var(--font-size-mini);
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
padding: var(--space-smaller) 0;
|
||||
align-items: center;
|
||||
|
||||
.date-wrap {
|
||||
margin-left: var(--space-smaller);
|
||||
padding: var(--space-micro);
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-micro);
|
||||
}
|
||||
}
|
||||
.actions {
|
||||
|
|
|
@ -1,39 +1,37 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="notelist-wrap">
|
||||
<h3 class="block-title">
|
||||
{{ $t('NOTES.HEADER.TITLE') }}
|
||||
</h3>
|
||||
<add-note @add="onAddNote" />
|
||||
<contact-note
|
||||
v-for="note in notes"
|
||||
:id="note.id"
|
||||
:key="note.id"
|
||||
:note="note.content"
|
||||
:user-name="note.user.name"
|
||||
:time-stamp="note.created_at"
|
||||
:thumbnail="note.user.thumbnail"
|
||||
@edit="onEditNote"
|
||||
@delete="onDeleteNote"
|
||||
/>
|
||||
<div class="button-wrap">
|
||||
<woot-button variant="link" @click="onclick">
|
||||
{{ $t('NOTES.FOOTER.BUTTON') }}
|
||||
<i class="ion-arrow-right-c" />
|
||||
</woot-button>
|
||||
</div>
|
||||
<add-note @add="onAddNote" />
|
||||
<contact-note
|
||||
v-for="note in notes"
|
||||
:id="note.id"
|
||||
:key="note.id"
|
||||
:note="note.content"
|
||||
:user="note.user"
|
||||
:created-at="note.created_at"
|
||||
@edit="onEditNote"
|
||||
@delete="onDeleteNote"
|
||||
/>
|
||||
|
||||
<div v-if="isFetching" class="text-center p-normal fs-default">
|
||||
<spinner size="" />
|
||||
<span>{{ $t('NOTES.FETCHING_NOTES') }}</span>
|
||||
</div>
|
||||
<div v-else-if="!notes.length" class="text-center p-normal fs-default">
|
||||
<span>{{ $t('NOTES.NOT_AVAILABLE') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContactNote from './ContactNote';
|
||||
import AddNote from './AddNote';
|
||||
import ContactNote from './ContactNote';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactNote,
|
||||
AddNote,
|
||||
ContactNote,
|
||||
Spinner,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -41,12 +39,13 @@ export default {
|
|||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isFetching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onclick() {
|
||||
this.$emit('show');
|
||||
},
|
||||
onAddNote(value) {
|
||||
this.$emit('add', value);
|
||||
},
|
||||
|
@ -59,9 +58,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button-wrap {
|
||||
margin-top: var(--space-one);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
<template>
|
||||
<div class="medium-3 bg-white contact--panel">
|
||||
<span class="close-button" @click="onClose">
|
||||
<div
|
||||
class="small-12 medium-3 bg-white contact--panel"
|
||||
:class="{ 'border-left': showAvatar }"
|
||||
>
|
||||
<span v-if="showAvatar" class="close-button" @click="onClose">
|
||||
<i class="ion-android-close close-icon" />
|
||||
</span>
|
||||
<contact-info show-new-message :contact="contact" @panel-close="onClose" />
|
||||
<contact-info
|
||||
:show-avatar="showAvatar"
|
||||
show-new-message
|
||||
:contact="contact"
|
||||
@panel-close="onClose"
|
||||
/>
|
||||
<accordion-item
|
||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CUSTOM_ATTRIBUTES')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
||||
|
@ -61,6 +69,10 @@ export default {
|
|||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasContactAttributes() {
|
||||
|
@ -85,7 +97,7 @@ export default {
|
|||
overflow-y: auto;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { VeTable } from 'vue-easytable';
|
||||
import flag from 'country-code-emoji';
|
||||
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
@ -94,10 +95,10 @@ export default {
|
|||
...item,
|
||||
phone_number: item.phone_number || '---',
|
||||
company: additional.company_name || '---',
|
||||
location: additional.location || '---',
|
||||
profiles: additional.social_profiles || {},
|
||||
city: additional.city || '---',
|
||||
country: additional.country || '---',
|
||||
country: additional.country,
|
||||
countryCode: additional.country_code,
|
||||
conversationsCount: item.conversations_count || '---',
|
||||
last_activity_at: lastActivityAt
|
||||
? this.dynamicTime(lastActivityAt)
|
||||
|
@ -128,12 +129,17 @@ export default {
|
|||
status={row.availability_status}
|
||||
/>
|
||||
<div class="user-block">
|
||||
<h6 class="sub-block-title user-name text-truncate">
|
||||
{row.name}
|
||||
<h6 class="sub-block-title text-truncate">
|
||||
<router-link
|
||||
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
|
||||
class="user-name"
|
||||
>
|
||||
{row.name}
|
||||
</router-link>
|
||||
</h6>
|
||||
<span class="button clear small link">
|
||||
<button class="button clear small link view-details--button">
|
||||
{this.$t('CONTACTS_PAGE.LIST.VIEW_DETAILS')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
|
@ -186,6 +192,16 @@ export default {
|
|||
key: 'country',
|
||||
title: this.$t('CONTACTS_PAGE.LIST.TABLE_HEADER.COUNTRY'),
|
||||
align: 'left',
|
||||
renderBodyCell: ({ row }) => {
|
||||
if (row.country) {
|
||||
return (
|
||||
<div class="text-truncate">
|
||||
{`${flag(row.countryCode)} ${row.country}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return '---';
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'profiles',
|
||||
|
@ -281,10 +297,15 @@ export default {
|
|||
|
||||
.user-name {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin: 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.view-details--button {
|
||||
color: var(--color-body);
|
||||
}
|
||||
|
||||
.user-email {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -1,81 +1,136 @@
|
|||
<template>
|
||||
<div class="contact-manage-view">
|
||||
<contacts-header
|
||||
:search-query="searchQuery"
|
||||
:on-search-submit="onSearchSubmit"
|
||||
:on-input-search="onInputSearch"
|
||||
:on-toggle-create="onToggleCreate"
|
||||
/>
|
||||
<manage-layout :contact-id="contactId" />
|
||||
<div class="view-box columns bg-white">
|
||||
<settings-header
|
||||
button-route="new"
|
||||
:header-title="contact.name"
|
||||
show-back-button
|
||||
:back-button-label="$t('CONTACT_PROFILE.BACK_BUTTON')"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="false"
|
||||
>
|
||||
<thumbnail
|
||||
v-if="contact.thumbnail"
|
||||
:src="contact.thumbnail"
|
||||
:username="contact.name"
|
||||
size="32px"
|
||||
class="margin-right-small"
|
||||
/>
|
||||
</settings-header>
|
||||
|
||||
<create-contact :show="showCreateModal" @cancel="onToggleCreate" />
|
||||
<div
|
||||
v-if="uiFlags.isFetchingItem"
|
||||
class="text-center p-normal fs-default h-full"
|
||||
>
|
||||
<spinner size="" />
|
||||
<span>{{ $t('CONTACT_PROFILE.LOADING') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="contact.id"
|
||||
class="overflow-hidden column contact--dashboard-content"
|
||||
>
|
||||
<div class="row h-full">
|
||||
<contact-info-panel :show-avatar="false" :contact="contact" />
|
||||
<div class="small-12 medium-9 h-full">
|
||||
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
<div class="tab-content overflow-auto">
|
||||
<contact-notes
|
||||
v-if="selectedTabIndex === 0"
|
||||
:contact-id="Number(contactId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactsHeader from '../components/Header';
|
||||
import ManageLayout from 'dashboard/modules/contact/components/ManageLayout';
|
||||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact';
|
||||
import ContactInfoPanel from '../components/ContactInfoPanel.vue';
|
||||
import ContactNotes from 'dashboard/modules/notes/NotesOnContactPage';
|
||||
import SettingsHeader from '../../settings/SettingsHeader.vue';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactsHeader,
|
||||
CreateContact,
|
||||
ManageLayout,
|
||||
ContactInfoPanel,
|
||||
ContactNotes,
|
||||
SettingsHeader,
|
||||
Spinner,
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
contactId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
showCreateModal: false,
|
||||
selectedTabIndex: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'contacts/getUIFlags',
|
||||
}),
|
||||
tabs() {
|
||||
return [
|
||||
{
|
||||
key: 0,
|
||||
name: this.$t('NOTES.HEADER.TITLE'),
|
||||
},
|
||||
];
|
||||
},
|
||||
showEmptySearchResult() {
|
||||
const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
|
||||
return hasEmptyResults;
|
||||
},
|
||||
contact() {
|
||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||
},
|
||||
backUrl() {
|
||||
return `/app/accounts/${this.$route.params.accountId}/contacts`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchContactDetails();
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
onInputSearch(event) {
|
||||
const newQuery = event.target.value;
|
||||
const refetchAllContacts = !!this.searchQuery && newQuery === '';
|
||||
if (refetchAllContacts) {
|
||||
this.$store.dispatch('contacts/get', { page: 1 });
|
||||
}
|
||||
this.searchQuery = newQuery;
|
||||
onClickTabChange(index) {
|
||||
this.selectedTabIndex = index;
|
||||
},
|
||||
onSearchSubmit() {
|
||||
this.selectedContactId = '';
|
||||
if (this.searchQuery) {
|
||||
this.$store.dispatch('contacts/search', {
|
||||
search: this.searchQuery,
|
||||
page: 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
onToggleCreate() {
|
||||
this.showCreateModal = !this.showCreateModal;
|
||||
fetchContactDetails() {
|
||||
const { contactId: id } = this;
|
||||
this.$store.dispatch('contacts/show', { id });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contact-manage-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1 1 0;
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.left {
|
||||
border-right: 1px solid var(--color-border);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.right {
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background: var(--color-background-light);
|
||||
height: calc(100% - 40px);
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -21,7 +21,7 @@ export const routes = [
|
|||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/:contactId'),
|
||||
name: 'contacts_dashboard_manage',
|
||||
name: 'contact_profile_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: ContactManageView,
|
||||
props: route => {
|
||||
|
|
|
@ -222,9 +222,8 @@ export default {
|
|||
return this.additionalAttributes.initiated_at;
|
||||
},
|
||||
browserName() {
|
||||
return `${this.browser.browser_name || ''} ${
|
||||
this.browser.browser_version || ''
|
||||
}`;
|
||||
return `${this.browser.browser_name || ''} ${this.browser
|
||||
.browser_version || ''}`;
|
||||
},
|
||||
contactAdditionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
|
@ -248,8 +247,10 @@ export default {
|
|||
return `${cityAndCountry} ${countryFlag}`;
|
||||
},
|
||||
platformName() {
|
||||
const { platform_name: platformName, platform_version: platformVersion } =
|
||||
this.browser;
|
||||
const {
|
||||
platform_name: platformName,
|
||||
platform_version: platformVersion,
|
||||
} = this.browser;
|
||||
return `${platformName || ''} ${platformVersion || ''}`;
|
||||
},
|
||||
channelType() {
|
||||
|
@ -403,7 +404,7 @@ export default {
|
|||
::v-deep {
|
||||
.contact--profile {
|
||||
padding-bottom: var(--space-slab);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.conversation--actions .multiselect-wrap--small {
|
||||
.multiselect {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<div class="contact--profile">
|
||||
<div class="contact--info">
|
||||
<thumbnail
|
||||
v-if="showAvatar"
|
||||
:src="contact.thumbnail"
|
||||
size="56px"
|
||||
:username="contact.name"
|
||||
|
@ -9,8 +10,16 @@
|
|||
/>
|
||||
|
||||
<div class="contact--details">
|
||||
<h3 class="sub-block-title contact--name">
|
||||
{{ contact.name }}
|
||||
<h3 v-if="showAvatar" class="sub-block-title contact--name">
|
||||
<a
|
||||
:href="contactProfileLink"
|
||||
class="fs-default"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
>
|
||||
{{ contact.name }}
|
||||
<i class="ion-android-open open-link--icon" />
|
||||
</a>
|
||||
</h3>
|
||||
<p v-if="additionalAttributes.description" class="contact--bio">
|
||||
{{ additionalAttributes.description }}
|
||||
|
@ -25,7 +34,6 @@
|
|||
:title="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
||||
show-copy
|
||||
/>
|
||||
|
||||
<contact-info-row
|
||||
:href="contact.phone_number ? `tel:${contact.phone_number}` : ''"
|
||||
:value="contact.phone_number"
|
||||
|
@ -161,6 +169,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -172,6 +184,9 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
|
||||
contactProfileLink() {
|
||||
return `/app/accounts/${this.$route.params.accountId}/contacts/${this.contact.id}`;
|
||||
},
|
||||
additionalAttributes() {
|
||||
return this.contact.additional_attributes || {};
|
||||
},
|
||||
|
@ -266,6 +281,16 @@ export default {
|
|||
.contact--name {
|
||||
text-transform: capitalize;
|
||||
white-space: normal;
|
||||
|
||||
a {
|
||||
color: var(--color-body);
|
||||
}
|
||||
|
||||
.open-link--icon {
|
||||
color: var(--color-body);
|
||||
font-size: var(--font-size-small);
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
|
||||
.contact--metadata {
|
||||
|
|
|
@ -2,8 +2,13 @@
|
|||
<div class="settings-header">
|
||||
<h1 class="page-title">
|
||||
<woot-sidemenu-icon></woot-sidemenu-icon>
|
||||
<back-button v-if="showBackButton" :back-url="backUrl"></back-button>
|
||||
<i :class="iconClass"></i>
|
||||
<back-button
|
||||
v-if="showBackButton"
|
||||
:button-label="backButtonLabel"
|
||||
:back-url="backUrl"
|
||||
/>
|
||||
<i v-if="icon" :class="iconClass"></i>
|
||||
<slot></slot>
|
||||
<span>{{ headerTitle }}</span>
|
||||
</h1>
|
||||
<router-link
|
||||
|
@ -51,6 +56,10 @@ export default {
|
|||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
backButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as types from '../mutation-types';
|
||||
import types from '../mutation-types';
|
||||
import Vue from 'vue';
|
||||
import ContactNotesAPI from '../../api/contactNotes';
|
||||
|
||||
|
@ -13,7 +13,8 @@ export const state = {
|
|||
|
||||
export const getters = {
|
||||
getAllNotesByContact: _state => contactId => {
|
||||
return _state.records[contactId] || [];
|
||||
const records = _state.records[contactId] || [];
|
||||
return records.sort((r1, r2) => r2.id - r1.id);
|
||||
},
|
||||
getUIFlags(_state) {
|
||||
return _state.uiFlags;
|
||||
|
@ -22,76 +23,58 @@ export const getters = {
|
|||
|
||||
export const actions = {
|
||||
async get({ commit }, { contactId }) {
|
||||
commit(types.default.SET_CONTACT_NOTES_UI_FLAG, {
|
||||
isFetching: true,
|
||||
});
|
||||
commit(types.SET_CONTACT_NOTES_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const { data } = await ContactNotesAPI.get(contactId);
|
||||
commit(types.default.SET_CONTACT_NOTES, { contactId, data });
|
||||
commit(types.SET_CONTACT_NOTES, { contactId, data });
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
commit(types.default.SET_CONTACT_NOTES_UI_FLAG, {
|
||||
isFetching: false,
|
||||
});
|
||||
commit(types.SET_CONTACT_NOTES_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
async create({ commit }, { contactId, content }) {
|
||||
commit(types.default.SET_CONTACT_NOTES_UI_FLAG, {
|
||||
isCreating: true,
|
||||
});
|
||||
commit(types.SET_CONTACT_NOTES_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const { data } = await ContactNotesAPI.create({
|
||||
content,
|
||||
contactId,
|
||||
});
|
||||
commit(types.default.ADD_CONTACT_NOTE, {
|
||||
contactId,
|
||||
data,
|
||||
});
|
||||
const { data } = await ContactNotesAPI.create(contactId, content);
|
||||
commit(types.ADD_CONTACT_NOTE, { contactId, data });
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
commit(types.default.SET_CONTACT_NOTES_UI_FLAG, {
|
||||
isCreating: false,
|
||||
});
|
||||
commit(types.SET_CONTACT_NOTES_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
},
|
||||
|
||||
async delete({ commit }, { noteId, contactId }) {
|
||||
commit(types.default.SET_CONTACT_NOTES_UI_FLAG, {
|
||||
isDeleting: true,
|
||||
});
|
||||
commit(types.SET_CONTACT_NOTES_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await ContactNotesAPI.delete(contactId, noteId);
|
||||
commit(types.default.DELETE_CONTACT_NOTE, { contactId, noteId });
|
||||
commit(types.DELETE_CONTACT_NOTE, { contactId, noteId });
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
commit(types.default.SET_CONTACT_NOTES_UI_FLAG, {
|
||||
isDeleting: false,
|
||||
});
|
||||
commit(types.SET_CONTACT_NOTES_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[types.default.SET_CONTACT_NOTES_UI_FLAG](_state, data) {
|
||||
[types.SET_CONTACT_NOTES_UI_FLAG](_state, data) {
|
||||
_state.uiFlags = {
|
||||
..._state.uiFlags,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
|
||||
[types.default.SET_CONTACT_NOTES]($state, { data, contactId }) {
|
||||
[types.SET_CONTACT_NOTES]($state, { data, contactId }) {
|
||||
Vue.set($state.records, contactId, data);
|
||||
},
|
||||
[types.default.ADD_CONTACT_NOTE]($state, { data, contactId }) {
|
||||
[types.ADD_CONTACT_NOTE]($state, { data, contactId }) {
|
||||
const contactNotes = $state.records[contactId] || [];
|
||||
$state.records[contactId] = [...contactNotes, data];
|
||||
},
|
||||
[types.default.DELETE_CONTACT_NOTE]($state, { noteId, contactId }) {
|
||||
[types.DELETE_CONTACT_NOTE]($state, { noteId, contactId }) {
|
||||
const contactNotes = $state.records[contactId];
|
||||
const withoutDeletedNote = contactNotes.filter(note => note.id !== noteId);
|
||||
$state.records[contactId] = [...withoutDeletedNote];
|
||||
|
|
|
@ -10,6 +10,10 @@ export const hasPressedShift = e => {
|
|||
return e.shiftKey;
|
||||
};
|
||||
|
||||
export const hasPressedCommandAndEnter = e => {
|
||||
return e.metaKey && e.keyCode === 13;
|
||||
};
|
||||
|
||||
export const hasPressedCommandAndForwardSlash = e => {
|
||||
return e.metaKey && e.keyCode === 191;
|
||||
};
|
||||
|
|
|
@ -33,6 +33,8 @@ class Note < ApplicationRecord
|
|||
belongs_to :contact
|
||||
belongs_to :user
|
||||
|
||||
scope :latest, -> { order(created_at: :desc) }
|
||||
|
||||
private
|
||||
|
||||
def ensure_account_id
|
||||
|
|
|
@ -2,8 +2,10 @@ json.id resource.id
|
|||
json.content resource.content
|
||||
json.account_id json.account_id
|
||||
json.contact_id json.contact_id
|
||||
json.user do
|
||||
json.partial! 'api/v1/models/agent.json.jbuilder', resource: resource.user
|
||||
if resource.user.present?
|
||||
json.user do
|
||||
json.partial! 'api/v1/models/agent.json.jbuilder', resource: resource.user
|
||||
end
|
||||
end
|
||||
json.created_at resource.created_at
|
||||
json.updated_at resource.updated_at
|
||||
json.created_at resource.created_at.to_i
|
||||
json.updated_at resource.updated_at.to_i
|
||||
|
|
|
@ -4309,9 +4309,9 @@ caniuse-api@^3.0.0:
|
|||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001214:
|
||||
version "1.0.30001219"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001219.tgz#5bfa5d0519f41f993618bd318f606a4c4c16156b"
|
||||
integrity sha512-c0yixVG4v9KBc/tQ2rlbB3A/bgBFRvl8h8M4IeUbqCca4gsiCfvtaheUssbnux/Mb66Vjz7x8yYjDgYcNQOhyQ==
|
||||
version "1.0.30001271"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz"
|
||||
integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
|
Loading…
Reference in a new issue