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