chore: Remove unused files in contact (#10570)
This commit is contained in:
@@ -106,10 +106,15 @@ const onImport = async file => {
|
|||||||
try {
|
try {
|
||||||
await store.dispatch('contacts/import', file);
|
await store.dispatch('contacts/import', file);
|
||||||
contactImportDialogRef.value?.dialogRef.close();
|
contactImportDialogRef.value?.dialogRef.close();
|
||||||
useAlert(t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
useAlert(
|
||||||
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.SUCCESS_MESSAGE')
|
||||||
|
);
|
||||||
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
useAlert(error.message ?? t('IMPORT_CONTACTS.ERROR_MESSAGE'));
|
useAlert(
|
||||||
|
error.message ??
|
||||||
|
t('CONTACTS_LAYOUT.HEADER.ACTIONS.IMPORT_CONTACT.ERROR_MESSAGE')
|
||||||
|
);
|
||||||
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
|
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,407 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
|
||||||
import FilterInputBox from '../FilterInput/Index.vue';
|
|
||||||
import languages from './advancedFilterItems/languages';
|
|
||||||
import countries from 'shared/constants/countries.js';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { filterAttributeGroups } from './advancedFilterItems';
|
|
||||||
import { useFilter } from 'shared/composables/useFilter';
|
|
||||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
|
||||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
|
||||||
import { useTrack } from 'dashboard/composables';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
FilterInputBox,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
onClose: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
initialFilterTypes: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
initialAppliedFilters: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
activeFolderName: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
isFolderView: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['applyFilter', 'updateFolder'],
|
|
||||||
setup() {
|
|
||||||
const { setFilterAttributes } = useFilter({
|
|
||||||
filteri18nKey: 'FILTER',
|
|
||||||
attributeModel: 'conversation_attribute',
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
setFilterAttributes,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
show: true,
|
|
||||||
appliedFilters: this.initialAppliedFilters,
|
|
||||||
activeFolderNewName: this.activeFolderName,
|
|
||||||
filterTypes: this.initialFilterTypes,
|
|
||||||
filterAttributeGroups,
|
|
||||||
filterGroups: [],
|
|
||||||
allCustomAttributes: [],
|
|
||||||
attributeModel: 'conversation_attribute',
|
|
||||||
filtersFori18n: 'FILTER',
|
|
||||||
validationErrors: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
getAppliedConversationFilters: 'getAppliedConversationFilters',
|
|
||||||
}),
|
|
||||||
filterModalHeaderTitle() {
|
|
||||||
return !this.isFolderView
|
|
||||||
? this.$t('FILTER.TITLE')
|
|
||||||
: this.$t('FILTER.EDIT_CUSTOM_FILTER');
|
|
||||||
},
|
|
||||||
filterModalSubTitle() {
|
|
||||||
return !this.isFolderView
|
|
||||||
? this.$t('FILTER.SUBTITLE')
|
|
||||||
: this.$t('FILTER.CUSTOM_VIEWS_SUBTITLE');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const { filterGroups, filterTypes } = this.setFilterAttributes();
|
|
||||||
|
|
||||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
|
||||||
this.filterGroups = filterGroups;
|
|
||||||
|
|
||||||
this.$store.dispatch('campaigns/get');
|
|
||||||
if (this.getAppliedConversationFilters.length) {
|
|
||||||
this.appliedFilters = [];
|
|
||||||
this.appliedFilters = [...this.getAppliedConversationFilters];
|
|
||||||
} else if (!this.isFolderView) {
|
|
||||||
this.appliedFilters.push({
|
|
||||||
attribute_key: 'status',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: '',
|
|
||||||
query_operator: 'and',
|
|
||||||
attribute_model: 'standard',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getOperatorTypes(key) {
|
|
||||||
switch (key) {
|
|
||||||
case 'list':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
case 'text':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_3;
|
|
||||||
case 'number':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
case 'link':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
case 'date':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_4;
|
|
||||||
case 'checkbox':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
default:
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
customAttributeInputType(key) {
|
|
||||||
switch (key) {
|
|
||||||
case 'date':
|
|
||||||
return 'date';
|
|
||||||
case 'text':
|
|
||||||
return 'plain_text';
|
|
||||||
case 'list':
|
|
||||||
return 'search_select';
|
|
||||||
case 'checkbox':
|
|
||||||
return 'search_select';
|
|
||||||
default:
|
|
||||||
return 'plain_text';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getAttributeModel(key) {
|
|
||||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
|
||||||
return type.attributeModel;
|
|
||||||
},
|
|
||||||
getInputType(key, operator) {
|
|
||||||
if (key === 'created_at' || key === 'last_activity_at')
|
|
||||||
if (operator === 'days_before') return 'plain_text';
|
|
||||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
|
||||||
return type?.inputType;
|
|
||||||
},
|
|
||||||
getOperators(key) {
|
|
||||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
|
||||||
return type?.filterOperators;
|
|
||||||
},
|
|
||||||
statusFilterItems() {
|
|
||||||
return {
|
|
||||||
open: {
|
|
||||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
|
|
||||||
},
|
|
||||||
resolved: {
|
|
||||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
|
|
||||||
},
|
|
||||||
pending: {
|
|
||||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
|
|
||||||
},
|
|
||||||
snoozed: {
|
|
||||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
|
|
||||||
},
|
|
||||||
all: {
|
|
||||||
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getDropdownValues(type) {
|
|
||||||
const statusFilters = this.statusFilterItems();
|
|
||||||
const allCustomAttributes = this.$store.getters[
|
|
||||||
'attributes/getAttributesByModel'
|
|
||||||
](this.attributeModel);
|
|
||||||
|
|
||||||
const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
|
|
||||||
return (
|
|
||||||
attr.attribute_key === type &&
|
|
||||||
attr.attribute_display_type === 'checkbox'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isCustomAttributeCheckbox) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: true,
|
|
||||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: false,
|
|
||||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCustomAttributeList = allCustomAttributes.find(attr => {
|
|
||||||
return (
|
|
||||||
attr.attribute_key === type && attr.attribute_display_type === 'list'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isCustomAttributeList) {
|
|
||||||
return allCustomAttributes
|
|
||||||
.find(attr => attr.attribute_key === type)
|
|
||||||
.attribute_values.map(item => {
|
|
||||||
return {
|
|
||||||
id: item,
|
|
||||||
name: item,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'status':
|
|
||||||
return [
|
|
||||||
...Object.keys(statusFilters).map(status => {
|
|
||||||
return {
|
|
||||||
id: status,
|
|
||||||
name: statusFilters[status].TEXT,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
case 'assignee_id':
|
|
||||||
return this.$store.getters['agents/getAgents'];
|
|
||||||
case 'contact':
|
|
||||||
return this.$store.getters['contacts/getContacts'];
|
|
||||||
case 'inbox_id':
|
|
||||||
return this.$store.getters['inboxes/getInboxes'];
|
|
||||||
case 'team_id':
|
|
||||||
return this.$store.getters['teams/getTeams'];
|
|
||||||
case 'campaign_id':
|
|
||||||
return this.$store.getters['campaigns/getAllCampaigns'].map(i => {
|
|
||||||
return {
|
|
||||||
id: i.id,
|
|
||||||
name: i.title,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
case 'labels':
|
|
||||||
return this.$store.getters['labels/getLabels'].map(i => {
|
|
||||||
return {
|
|
||||||
id: i.title,
|
|
||||||
name: i.title,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
case 'browser_language':
|
|
||||||
return languages;
|
|
||||||
case 'country_code':
|
|
||||||
return countries;
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
appendNewFilter() {
|
|
||||||
if (this.isFolderView) {
|
|
||||||
this.setQueryOperatorOnLastQuery();
|
|
||||||
} else {
|
|
||||||
this.appliedFilters.push({
|
|
||||||
attribute_key: 'status',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: '',
|
|
||||||
query_operator: 'and',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setQueryOperatorOnLastQuery() {
|
|
||||||
const lastItemIndex = this.appliedFilters.length - 1;
|
|
||||||
this.appliedFilters[lastItemIndex] = {
|
|
||||||
...this.appliedFilters[lastItemIndex],
|
|
||||||
query_operator: 'and',
|
|
||||||
};
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.appliedFilters.push({
|
|
||||||
attribute_key: 'status',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: '',
|
|
||||||
query_operator: 'and',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeFilter(index) {
|
|
||||||
if (this.appliedFilters.length <= 1) {
|
|
||||||
useAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
|
|
||||||
} else {
|
|
||||||
this.appliedFilters.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submitFilterQuery() {
|
|
||||||
this.validationErrors = validateConversationOrContactFilters(
|
|
||||||
this.appliedFilters
|
|
||||||
);
|
|
||||||
if (Object.keys(this.validationErrors).length === 0) {
|
|
||||||
this.$store.dispatch(
|
|
||||||
'setConversationFilters',
|
|
||||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
|
||||||
);
|
|
||||||
this.$emit('applyFilter', this.appliedFilters);
|
|
||||||
useTrack(CONVERSATION_EVENTS.APPLY_FILTER, {
|
|
||||||
applied_filters: this.appliedFilters.map(filter => ({
|
|
||||||
key: filter.attribute_key,
|
|
||||||
operator: filter.filter_operator,
|
|
||||||
query_operator: filter.query_operator,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateSavedCustomViews() {
|
|
||||||
this.$emit('updateFolder', this.appliedFilters, this.activeFolderNewName);
|
|
||||||
},
|
|
||||||
resetFilter(index, currentFilter) {
|
|
||||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
|
||||||
filter => filter.attributeKey === currentFilter.attribute_key
|
|
||||||
).filterOperators[0].value;
|
|
||||||
this.appliedFilters[index].values = '';
|
|
||||||
},
|
|
||||||
showUserInput(operatorType) {
|
|
||||||
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
|
||||||
<p class="text-slate-600 dark:text-slate-200">
|
|
||||||
{{ filterModalSubTitle }}
|
|
||||||
</p>
|
|
||||||
</woot-modal-header>
|
|
||||||
<div class="p-8">
|
|
||||||
<div v-if="isFolderView">
|
|
||||||
<label class="input-label" :class="{ error: !activeFolderNewName }">
|
|
||||||
{{ $t('FILTER.FOLDER_LABEL') }}
|
|
||||||
<input
|
|
||||||
v-model="activeFolderNewName"
|
|
||||||
type="text"
|
|
||||||
class="bg-white folder-input border-slate-75 dark:border-slate-600 dark:bg-slate-900 text-slate-900 dark:text-slate-100"
|
|
||||||
/>
|
|
||||||
<span v-if="!activeFolderNewName" class="message">
|
|
||||||
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label class="mb-1">
|
|
||||||
{{ $t('FILTER.FOLDER_QUERY_LABEL') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="p-4 mb-4 border border-solid rounded-lg bg-slate-25 dark:bg-slate-900 border-slate-50 dark:border-slate-700/50"
|
|
||||||
>
|
|
||||||
<FilterInputBox
|
|
||||||
v-for="(filter, i) in appliedFilters"
|
|
||||||
:key="i"
|
|
||||||
v-model="appliedFilters[i]"
|
|
||||||
:filter-groups="filterGroups"
|
|
||||||
:input-type="
|
|
||||||
getInputType(
|
|
||||||
appliedFilters[i].attribute_key,
|
|
||||||
appliedFilters[i].filter_operator
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:operators="getOperators(appliedFilters[i].attribute_key)"
|
|
||||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
|
||||||
:show-query-operator="i !== appliedFilters.length - 1"
|
|
||||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
|
||||||
grouped-filters
|
|
||||||
:error-message="
|
|
||||||
validationErrors[`filter_${i}`]
|
|
||||||
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
@reset-filter="resetFilter(i, appliedFilters[i])"
|
|
||||||
@remove-filter="removeFilter(i)"
|
|
||||||
/>
|
|
||||||
<div class="mt-4">
|
|
||||||
<woot-button
|
|
||||||
icon="add"
|
|
||||||
color-scheme="success"
|
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
@click="appendNewFilter"
|
|
||||||
>
|
|
||||||
{{ $t('FILTER.ADD_NEW_FILTER') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
|
||||||
<woot-button class="button clear" @click.prevent="onClose">
|
|
||||||
{{ $t('FILTER.CANCEL_BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
v-if="isFolderView"
|
|
||||||
:disabled="!activeFolderNewName"
|
|
||||||
@click="updateSavedCustomViews"
|
|
||||||
>
|
|
||||||
{{ $t('FILTER.UPDATE_BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button v-else @click="submitFilterQuery">
|
|
||||||
{{ $t('FILTER.SUBMIT_BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.folder-input {
|
|
||||||
@apply w-[50%];
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,46 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import { validateAutomation } from '../validations';
|
||||||
validateConversationOrContactFilters,
|
|
||||||
validateAutomation,
|
|
||||||
} from '../validations';
|
|
||||||
|
|
||||||
describe('validateConversationOrContactFilters', () => {
|
|
||||||
it('should return no errors for valid filters', () => {
|
|
||||||
const validFilters = [
|
|
||||||
{ attribute_key: 'name', filter_operator: 'contains', values: 'John' },
|
|
||||||
{ attribute_key: 'email', filter_operator: 'is_present' },
|
|
||||||
];
|
|
||||||
const errors = validateConversationOrContactFilters(validFilters);
|
|
||||||
expect(errors).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return errors for invalid filters', () => {
|
|
||||||
const invalidFilters = [
|
|
||||||
{ attribute_key: '', filter_operator: 'contains', values: 'John' },
|
|
||||||
{ attribute_key: 'email', filter_operator: '' },
|
|
||||||
{ attribute_key: 'age', filter_operator: 'equals' },
|
|
||||||
];
|
|
||||||
const errors = validateConversationOrContactFilters(invalidFilters);
|
|
||||||
expect(errors).toEqual({
|
|
||||||
filter_0: 'ATTRIBUTE_KEY_REQUIRED',
|
|
||||||
filter_1: 'FILTER_OPERATOR_REQUIRED',
|
|
||||||
filter_2: 'VALUE_REQUIRED',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should validate days_before operator correctly', () => {
|
|
||||||
const filters = [
|
|
||||||
{ attribute_key: 'date', filter_operator: 'days_before', values: '0' },
|
|
||||||
{ attribute_key: 'date', filter_operator: 'days_before', values: '999' },
|
|
||||||
{ attribute_key: 'date', filter_operator: 'days_before', values: '500' },
|
|
||||||
];
|
|
||||||
const errors = validateConversationOrContactFilters(filters);
|
|
||||||
expect(errors).toEqual({
|
|
||||||
filter_0: 'VALUE_MUST_BE_BETWEEN_1_AND_998',
|
|
||||||
filter_1: 'VALUE_MUST_BE_BETWEEN_1_AND_998',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('validateAutomation', () => {
|
describe('validateAutomation', () => {
|
||||||
it('should return no errors for a valid automation', () => {
|
it('should return no errors for a valid automation', () => {
|
||||||
|
|||||||
@@ -65,29 +65,6 @@ export const validateSingleFilter = filter => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates filters for conversations or contacts.
|
|
||||||
*
|
|
||||||
* @param {Array} filters - An array of filter objects to validate.
|
|
||||||
* @param {string} filters[].attribute_key - The key of the attribute to filter on.
|
|
||||||
* @param {string} filters[].filter_operator - The operator to use for filtering.
|
|
||||||
* @param {string|number} [filters[].values] - The value(s) to filter by (required for most operators).
|
|
||||||
*
|
|
||||||
* @returns {Object} An object containing any validation errors, keyed by filter index.
|
|
||||||
*/
|
|
||||||
export const validateConversationOrContactFilters = filters => {
|
|
||||||
const errors = {};
|
|
||||||
|
|
||||||
filters.forEach((filter, index) => {
|
|
||||||
const error = validateSingleFilter(filter);
|
|
||||||
if (error) {
|
|
||||||
errors[`filter_${index}`] = error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// ---------------------- Automation Validation ---------------------
|
// ---------------------- Automation Validation ---------------------
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -57,46 +57,6 @@
|
|||||||
"TITLE": "Edit contact",
|
"TITLE": "Edit contact",
|
||||||
"DESC": "Edit contact details"
|
"DESC": "Edit contact details"
|
||||||
},
|
},
|
||||||
"CREATE_CONTACT": {
|
|
||||||
"BUTTON_LABEL": "New Contact",
|
|
||||||
"TITLE": "Create new contact",
|
|
||||||
"DESC": "Add basic information details about the contact."
|
|
||||||
},
|
|
||||||
"IMPORT_CONTACTS": {
|
|
||||||
"BUTTON_LABEL": "Import",
|
|
||||||
"TITLE": "Import Contacts",
|
|
||||||
"DESC": "Import contacts through a CSV file.",
|
|
||||||
"DOWNLOAD_LABEL": "Download a sample csv.",
|
|
||||||
"FORM": {
|
|
||||||
"LABEL": "CSV File",
|
|
||||||
"SUBMIT": "Import",
|
|
||||||
"CANCEL": "Cancel"
|
|
||||||
},
|
|
||||||
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
|
||||||
"ERROR_MESSAGE": "There was an error, please try again"
|
|
||||||
},
|
|
||||||
"EXPORT_CONTACTS": {
|
|
||||||
"BUTTON_LABEL": "Export",
|
|
||||||
"TITLE": "Export Contacts",
|
|
||||||
"DESC": "Export contacts to a CSV file.",
|
|
||||||
"SUCCESS_MESSAGE": "Export is in progress. You will be notified on email when the export file is ready to download.",
|
|
||||||
"ERROR_MESSAGE": "There was an error, please try again",
|
|
||||||
"CONFIRM": {
|
|
||||||
"TITLE": "Export Contacts",
|
|
||||||
"MESSAGE": "Are you sure you want to export all contacts?",
|
|
||||||
"FILTERED_MESSAGE": "Are you sure you want to export all the filtered contacts?",
|
|
||||||
"YES": "Yes, Export",
|
|
||||||
"NO": "No, Cancel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DELETE_NOTE": {
|
|
||||||
"CONFIRM": {
|
|
||||||
"TITLE": "Confirm Deletion",
|
|
||||||
"MESSAGE": "Are you want sure to delete this note?",
|
|
||||||
"YES": "Yes, Delete it",
|
|
||||||
"NO": "No, Keep it"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DELETE_CONTACT": {
|
"DELETE_CONTACT": {
|
||||||
"BUTTON_LABEL": "Delete Contact",
|
"BUTTON_LABEL": "Delete Contact",
|
||||||
"TITLE": "Delete contact",
|
"TITLE": "Delete contact",
|
||||||
@@ -224,79 +184,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CONTACTS_PAGE": {
|
"CONTACTS_PAGE": {
|
||||||
"HEADER": "Contacts",
|
|
||||||
"FIELDS": "Contact fields",
|
|
||||||
"SEARCH_BUTTON": "Search",
|
|
||||||
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts",
|
|
||||||
"FILTER_CONTACTS": "Filter",
|
|
||||||
"FILTER_CONTACTS_SAVE": "Save filter",
|
|
||||||
"FILTER_CONTACTS_DELETE": "Delete filter",
|
|
||||||
"FILTER_CONTACTS_EDIT": "Edit segment",
|
|
||||||
"LIST": {
|
"LIST": {
|
||||||
"LOADING_MESSAGE": "Loading contacts...",
|
|
||||||
"404": "No contacts matches your search 🔍",
|
|
||||||
"NO_CONTACTS": "There are no available contacts",
|
|
||||||
"TABLE_HEADER": {
|
"TABLE_HEADER": {
|
||||||
"NAME": "Name",
|
"SOCIAL_PROFILES": "Social Profiles"
|
||||||
"PHONE_NUMBER": "Phone Number",
|
}
|
||||||
"CONVERSATIONS": "Conversations",
|
|
||||||
"LAST_ACTIVITY": "Last Activity",
|
|
||||||
"CREATED_AT": "Created At",
|
|
||||||
"COUNTRY": "Country",
|
|
||||||
"CITY": "City",
|
|
||||||
"SOCIAL_PROFILES": "Social Profiles",
|
|
||||||
"COMPANY": "Company",
|
|
||||||
"EMAIL_ADDRESS": "Email Address"
|
|
||||||
},
|
|
||||||
"VIEW_DETAILS": "View details"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"CONTACT_PROFILE": {
|
|
||||||
"BACK_BUTTON": "Contacts",
|
|
||||||
"LOADING": "Loading contact profile..."
|
|
||||||
},
|
|
||||||
"REMINDER": {
|
|
||||||
"ADD_BUTTON": {
|
|
||||||
"BUTTON": "Add",
|
|
||||||
"TITLE": "Shift + Enter to create a task"
|
|
||||||
},
|
|
||||||
"FOOTER": {
|
|
||||||
"DUE_DATE": "Due date",
|
|
||||||
"LABEL_TITLE": "Set type"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"CONTENT_HEADER": {
|
|
||||||
"DELETE": "Delete note"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"EVENTS": {
|
|
||||||
"HEADER": {
|
|
||||||
"TITLE": "Activities"
|
|
||||||
},
|
|
||||||
"BUTTON": {
|
|
||||||
"PILL_BUTTON_NOTES": "notes",
|
|
||||||
"PILL_BUTTON_EVENTS": "events",
|
|
||||||
"PILL_BUTTON_CONVO": "conversations"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CUSTOM_ATTRIBUTES": {
|
"CUSTOM_ATTRIBUTES": {
|
||||||
"ADD_BUTTON_TEXT": "Add attributes",
|
|
||||||
"BUTTON": "Add custom attribute",
|
"BUTTON": "Add custom attribute",
|
||||||
"NOT_AVAILABLE": "There are no custom attributes available for this contact.",
|
|
||||||
"COPY_SUCCESSFUL": "Copied to clipboard successfully",
|
"COPY_SUCCESSFUL": "Copied to clipboard successfully",
|
||||||
"SHOW_MORE": "Show all attributes",
|
"SHOW_MORE": "Show all attributes",
|
||||||
"SHOW_LESS": "Show less attributes",
|
"SHOW_LESS": "Show less attributes",
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Modal from 'dashboard/components/Modal.vue';
|
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
|
||||||
import { required, minLength } from '@vuelidate/validators';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Modal,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isCreating: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['create', 'cancel', 'update:show'],
|
|
||||||
setup() {
|
|
||||||
return { v$: useVuelidate() };
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
attributeValue: '',
|
|
||||||
attributeName: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
validations: {
|
|
||||||
attributeName: {
|
|
||||||
required,
|
|
||||||
minLength: minLength(2),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
localShow: {
|
|
||||||
get() {
|
|
||||||
return this.show;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$emit('update:show', value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attributeNameError() {
|
|
||||||
if (this.v$.attributeName.$error) {
|
|
||||||
return this.$t('CUSTOM_ATTRIBUTES.FORM.NAME.ERROR');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
addCustomAttribute() {
|
|
||||||
this.$emit('create', {
|
|
||||||
attributeValue: this.attributeValue,
|
|
||||||
attributeName: this.attributeName || '',
|
|
||||||
});
|
|
||||||
this.reset();
|
|
||||||
this.$emit('cancel');
|
|
||||||
},
|
|
||||||
onClose() {
|
|
||||||
this.reset();
|
|
||||||
this.$emit('cancel');
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
this.attributeValue = '';
|
|
||||||
this.attributeName = '';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal v-model:show="localShow" :on-close="onClose">
|
|
||||||
<woot-modal-header
|
|
||||||
:header-title="$t('CUSTOM_ATTRIBUTES.ADD.TITLE')"
|
|
||||||
:header-content="$t('CUSTOM_ATTRIBUTES.ADD.DESC')"
|
|
||||||
/>
|
|
||||||
<form class="w-full" @submit.prevent="addCustomAttribute">
|
|
||||||
<woot-input
|
|
||||||
v-model="attributeName"
|
|
||||||
:class="{ error: v$.attributeName.$error }"
|
|
||||||
class="w-full"
|
|
||||||
:error="attributeNameError"
|
|
||||||
:label="$t('CUSTOM_ATTRIBUTES.FORM.NAME.LABEL')"
|
|
||||||
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.NAME.PLACEHOLDER')"
|
|
||||||
@blur="v$.attributeName.$touch"
|
|
||||||
@input="v$.attributeName.$touch"
|
|
||||||
/>
|
|
||||||
<woot-input
|
|
||||||
v-model="attributeValue"
|
|
||||||
class="w-full"
|
|
||||||
:label="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.LABEL')"
|
|
||||||
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.PLACEHOLDER')"
|
|
||||||
/>
|
|
||||||
<div class="flex items-center justify-end gap-2 px-0 py-2">
|
|
||||||
<woot-button
|
|
||||||
:is-disabled="v$.attributeName.$invalid || isCreating"
|
|
||||||
:is-loading="isCreating"
|
|
||||||
>
|
|
||||||
{{ $t('CUSTOM_ATTRIBUTES.FORM.CREATE') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button variant="clear" @click.prevent="onClose">
|
|
||||||
{{ $t('CUSTOM_ATTRIBUTES.FORM.CANCEL') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
<script>
|
|
||||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
EmojiOrIcon,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
label: { type: String, required: true },
|
|
||||||
icon: { type: String, default: '' },
|
|
||||||
emoji: { type: String, default: '' },
|
|
||||||
value: { type: [String, Number], default: '' },
|
|
||||||
showEdit: { type: Boolean, default: false },
|
|
||||||
},
|
|
||||||
emits: ['update'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isEditing: false,
|
|
||||||
editedValue: this.value,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
focusInput() {
|
|
||||||
this.$refs.inputfield.focus();
|
|
||||||
},
|
|
||||||
onEdit() {
|
|
||||||
this.isEditing = true;
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.focusInput();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onUpdate() {
|
|
||||||
this.isEditing = false;
|
|
||||||
this.$emit('update', this.editedValue);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="contact-attribute">
|
|
||||||
<div class="title-wrap">
|
|
||||||
<h4 class="text-sm title">
|
|
||||||
<div class="title--icon">
|
|
||||||
<EmojiOrIcon :icon="icon" :emoji="emoji" />
|
|
||||||
</div>
|
|
||||||
{{ label }}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div v-show="isEditing">
|
|
||||||
<div class="mb-2 w-full flex items-center">
|
|
||||||
<input
|
|
||||||
ref="inputfield"
|
|
||||||
v-model="editedValue"
|
|
||||||
type="text"
|
|
||||||
class="!h-8 ltr:rounded-r-none rtl:rounded-l-none !mb-0 !text-sm"
|
|
||||||
autofocus="true"
|
|
||||||
@keyup.enter="onUpdate"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<woot-button
|
|
||||||
size="small"
|
|
||||||
icon="ion-checkmark"
|
|
||||||
class="rounded-l-none rtl:rounded-r-none"
|
|
||||||
@click="onUpdate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="!isEditing"
|
|
||||||
class="value--view"
|
|
||||||
:class="{ 'is-editable': showEdit }"
|
|
||||||
>
|
|
||||||
<p class="value">
|
|
||||||
{{ value || '---' }}
|
|
||||||
</p>
|
|
||||||
<woot-button
|
|
||||||
v-if="showEdit"
|
|
||||||
variant="clear link"
|
|
||||||
size="small"
|
|
||||||
color-scheme="secondary"
|
|
||||||
icon="edit"
|
|
||||||
class-names="edit-button"
|
|
||||||
@click="onEdit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.contact-attribute {
|
|
||||||
margin-bottom: var(--space-small);
|
|
||||||
}
|
|
||||||
.title-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--space-mini);
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.title--icon {
|
|
||||||
width: var(--space-two);
|
|
||||||
}
|
|
||||||
.edit-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.value--view {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&.is-editable:hover {
|
|
||||||
.value {
|
|
||||||
background: var(--color-background);
|
|
||||||
}
|
|
||||||
.edit-button {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: var(--space-mega);
|
|
||||||
border-radius: var(--border-radius-small);
|
|
||||||
word-break: break-all;
|
|
||||||
margin: 0 var(--space-smaller) 0 var(--space-normal);
|
|
||||||
padding: var(--space-micro) var(--space-smaller);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Attribute from './ContactAttribute.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { Attribute },
|
|
||||||
props: {
|
|
||||||
contact: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update', 'createAttribute'],
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
additionalAttributes() {
|
|
||||||
return this.contact.additional_attributes || {};
|
|
||||||
},
|
|
||||||
company() {
|
|
||||||
const { company = {} } = this.contact;
|
|
||||||
return company;
|
|
||||||
},
|
|
||||||
customAttributes() {
|
|
||||||
const { custom_attributes: customAttributes = {} } = this.contact;
|
|
||||||
return customAttributes;
|
|
||||||
},
|
|
||||||
customAttributekeys() {
|
|
||||||
return Object.keys(this.customAttributes).filter(key => {
|
|
||||||
const value = this.customAttributes[key];
|
|
||||||
return value !== null && value !== undefined;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onEmailUpdate(value) {
|
|
||||||
this.$emit('update', { email: value });
|
|
||||||
},
|
|
||||||
onPhoneUpdate(value) {
|
|
||||||
this.$emit('update', { phone_number: value });
|
|
||||||
},
|
|
||||||
onLocationUpdate(value) {
|
|
||||||
this.$emit('update', { location: value });
|
|
||||||
},
|
|
||||||
handleCustomCreate() {
|
|
||||||
this.$emit('createAttribute');
|
|
||||||
},
|
|
||||||
onCustomAttributeUpdate(key, value) {
|
|
||||||
this.$emit('update', { custom_attributes: { [key]: value } });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="contact-fields">
|
|
||||||
<h3 class="text-lg title">
|
|
||||||
{{ $t('CONTACTS_PAGE.FIELDS') }}
|
|
||||||
</h3>
|
|
||||||
<Attribute
|
|
||||||
:label="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
|
||||||
icon="mail"
|
|
||||||
emoji=""
|
|
||||||
:value="contact.email"
|
|
||||||
show-edit
|
|
||||||
@update="onEmailUpdate"
|
|
||||||
/>
|
|
||||||
<Attribute
|
|
||||||
:label="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
|
||||||
icon="call"
|
|
||||||
emoji=""
|
|
||||||
:value="contact.phone_number"
|
|
||||||
show-edit
|
|
||||||
@update="onPhoneUpdate"
|
|
||||||
/>
|
|
||||||
<Attribute
|
|
||||||
v-if="additionalAttributes.location"
|
|
||||||
:label="$t('CONTACT_PANEL.LOCATION')"
|
|
||||||
icon="map"
|
|
||||||
emoji="🌍"
|
|
||||||
:value="additionalAttributes.location"
|
|
||||||
show-edit
|
|
||||||
@update="onLocationUpdate"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-for="attribute in customAttributekeys"
|
|
||||||
:key="attribute"
|
|
||||||
class="custom-attribute--row"
|
|
||||||
>
|
|
||||||
<Attribute
|
|
||||||
:label="attribute"
|
|
||||||
icon="chevron-right"
|
|
||||||
:value="customAttributes[attribute]"
|
|
||||||
show-edit
|
|
||||||
@update="value => onCustomAttributeUpdate(attribute, value)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<woot-button
|
|
||||||
size="small"
|
|
||||||
variant="link"
|
|
||||||
icon="add"
|
|
||||||
@click="handleCustomCreate"
|
|
||||||
>
|
|
||||||
{{ $t('CUSTOM_ATTRIBUTES.ADD.TITLE') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.contact-fields {
|
|
||||||
margin-top: var(--space-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-bottom: var(--space-normal);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
<script>
|
|
||||||
import EditContact from 'dashboard/routes/dashboard/conversation/contact/EditContact.vue';
|
|
||||||
import NewConversation from 'dashboard/routes/dashboard/conversation/contact/NewConversation.vue';
|
|
||||||
import AddCustomAttribute from 'dashboard/modules/contact/components/AddCustomAttribute.vue';
|
|
||||||
import ContactIntro from './ContactIntro.vue';
|
|
||||||
import ContactFields from './ContactFields.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
AddCustomAttribute,
|
|
||||||
ContactFields,
|
|
||||||
ContactIntro,
|
|
||||||
EditContact,
|
|
||||||
NewConversation,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
contact: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showCustomAttributeModal: false,
|
|
||||||
showEditModal: false,
|
|
||||||
showConversationModal: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
enableNewConversation() {
|
|
||||||
return this.contact && this.contact.id;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleCustomAttributeModal() {
|
|
||||||
this.showCustomAttributeModal = !this.showCustomAttributeModal;
|
|
||||||
},
|
|
||||||
toggleEditModal() {
|
|
||||||
this.showEditModal = !this.showEditModal;
|
|
||||||
},
|
|
||||||
toggleConversationModal() {
|
|
||||||
this.showConversationModal = !this.showConversationModal;
|
|
||||||
},
|
|
||||||
createCustomAttribute(data) {
|
|
||||||
const { id } = this.contact;
|
|
||||||
const { attributeValue, attributeName } = data;
|
|
||||||
const updatedFields = {
|
|
||||||
id,
|
|
||||||
custom_attributes: {
|
|
||||||
[attributeName]: attributeValue,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.updateContact(updatedFields);
|
|
||||||
},
|
|
||||||
updateField(data) {
|
|
||||||
const { id } = this.contact;
|
|
||||||
const updatedFields = {
|
|
||||||
id,
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
this.updateContact(updatedFields);
|
|
||||||
},
|
|
||||||
updateContact(contactItem) {
|
|
||||||
this.$store.dispatch('contacts/update', contactItem);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="panel">
|
|
||||||
<ContactIntro
|
|
||||||
:contact="contact"
|
|
||||||
@message="toggleConversationModal"
|
|
||||||
@edit="toggleEditModal"
|
|
||||||
/>
|
|
||||||
<ContactFields
|
|
||||||
:contact="contact"
|
|
||||||
@update="updateField"
|
|
||||||
@create-attribute="toggleCustomAttributeModal"
|
|
||||||
/>
|
|
||||||
<EditContact
|
|
||||||
v-if="showEditModal"
|
|
||||||
:show="showEditModal"
|
|
||||||
:contact="contact"
|
|
||||||
@cancel="toggleEditModal"
|
|
||||||
/>
|
|
||||||
<NewConversation
|
|
||||||
v-if="enableNewConversation"
|
|
||||||
:show="showConversationModal"
|
|
||||||
:contact="contact"
|
|
||||||
@cancel="toggleConversationModal"
|
|
||||||
/>
|
|
||||||
<AddCustomAttribute
|
|
||||||
:show="showCustomAttributeModal"
|
|
||||||
@cancel="toggleCustomAttributeModal"
|
|
||||||
@create="createCustomAttribute"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.panel {
|
|
||||||
padding: var(--space-normal) var(--space-normal);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import NoteList from './components/NoteList.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
NoteList,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
contactId: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ uiFlags: 'contactNotes/getUIFlags' }),
|
|
||||||
notes() {
|
|
||||||
return this.$store.getters['contactNotes/getAllNotesByContact'](
|
|
||||||
this.contactId
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fetchContactNotes();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchContactNotes() {
|
|
||||||
const { contactId } = this;
|
|
||||||
if (contactId) this.$store.dispatch('contactNotes/get', { contactId });
|
|
||||||
},
|
|
||||||
onAdd(content) {
|
|
||||||
const { contactId } = this;
|
|
||||||
this.$store.dispatch('contactNotes/create', { content, contactId });
|
|
||||||
},
|
|
||||||
onDelete(noteId) {
|
|
||||||
const { contactId } = this;
|
|
||||||
this.$store.dispatch('contactNotes/delete', { noteId, contactId });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NoteList
|
|
||||||
:is-fetching="uiFlags.isFetching"
|
|
||||||
:notes="notes"
|
|
||||||
@add="onAdd"
|
|
||||||
@delete="onDelete"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['add']);
|
|
||||||
|
|
||||||
const noteContent = ref('');
|
|
||||||
|
|
||||||
const buttonDisabled = computed(() => noteContent.value === '');
|
|
||||||
|
|
||||||
const onAdd = () => {
|
|
||||||
if (noteContent.value !== '') {
|
|
||||||
emit('add', noteContent.value);
|
|
||||||
}
|
|
||||||
noteContent.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyboardEvents = {
|
|
||||||
'$mod+Enter': {
|
|
||||||
action: () => onAdd(),
|
|
||||||
allowOnFocusedInput: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
useKeyboardEvents(keyboardEvents);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100"
|
|
||||||
>
|
|
||||||
<WootMessageEditor
|
|
||||||
v-model="noteContent"
|
|
||||||
class="input--note"
|
|
||||||
:placeholder="$t('NOTES.ADD.PLACEHOLDER')"
|
|
||||||
:enable-suggestions="false"
|
|
||||||
/>
|
|
||||||
<div class="flex justify-end w-full">
|
|
||||||
<woot-button
|
|
||||||
color-scheme="warning"
|
|
||||||
:title="$t('NOTES.ADD.TITLE')"
|
|
||||||
:is-disabled="buttonDisabled"
|
|
||||||
@click="onAdd"
|
|
||||||
>
|
|
||||||
{{ $t('NOTES.ADD.BUTTON') }} {{ '(⌘⏎)' }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.input--note {
|
|
||||||
&::v-deep .ProseMirror-menubar {
|
|
||||||
padding: 0;
|
|
||||||
margin-top: var(--space-minus-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::v-deep .ProseMirror-woot-style {
|
|
||||||
max-height: 22.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
<script>
|
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
|
||||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
|
||||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Thumbnail,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
note: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['delete'],
|
|
||||||
setup() {
|
|
||||||
const { formatMessage } = useMessageFormatter();
|
|
||||||
return {
|
|
||||||
formatMessage,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showDeleteModal: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
readableTime() {
|
|
||||||
return dynamicTime(this.createdAt);
|
|
||||||
},
|
|
||||||
noteAuthor() {
|
|
||||||
return this.user || {};
|
|
||||||
},
|
|
||||||
noteAuthorName() {
|
|
||||||
return this.noteAuthor.name || this.$t('APP_GLOBAL.DELETED_USER');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
toggleDeleteModal() {
|
|
||||||
this.showDeleteModal = !this.showDeleteModal;
|
|
||||||
},
|
|
||||||
onDelete() {
|
|
||||||
this.$emit('delete', this.id);
|
|
||||||
},
|
|
||||||
confirmDeletion() {
|
|
||||||
this.onDelete();
|
|
||||||
this.closeDelete();
|
|
||||||
},
|
|
||||||
closeDelete() {
|
|
||||||
this.showDeleteModal = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
|
|
||||||
>
|
|
||||||
<div class="flex items-end justify-between gap-1 text-xs">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Thumbnail
|
|
||||||
:title="noteAuthorName"
|
|
||||||
:src="noteAuthor.thumbnail"
|
|
||||||
:username="noteAuthorName"
|
|
||||||
size="20px"
|
|
||||||
/>
|
|
||||||
<div class="my-0 mx-1 p-0.5 flex flex-row gap-1">
|
|
||||||
<span class="font-medium text-slate-800 dark:text-slate-100">
|
|
||||||
{{ noteAuthorName }}
|
|
||||||
</span>
|
|
||||||
<span class="text-slate-700 dark:text-slate-100">
|
|
||||||
{{ $t('NOTES.LIST.LABEL') }}
|
|
||||||
</span>
|
|
||||||
<span class="font-medium text-slate-700 dark:text-slate-100">
|
|
||||||
{{ readableTime }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex invisible actions">
|
|
||||||
<woot-button
|
|
||||||
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
|
|
||||||
variant="smooth"
|
|
||||||
size="tiny"
|
|
||||||
icon="delete"
|
|
||||||
color-scheme="secondary"
|
|
||||||
@click="toggleDeleteModal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<woot-delete-modal
|
|
||||||
v-if="showDeleteModal"
|
|
||||||
v-model:show="showDeleteModal"
|
|
||||||
:on-close="closeDelete"
|
|
||||||
:on-confirm="confirmDeletion"
|
|
||||||
:title="$t('DELETE_NOTE.CONFIRM.TITLE')"
|
|
||||||
:message="$t('DELETE_NOTE.CONFIRM.MESSAGE')"
|
|
||||||
:confirm-text="$t('DELETE_NOTE.CONFIRM.YES')"
|
|
||||||
:reject-text="$t('DELETE_NOTE.CONFIRM.NO')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
v-dompurify-html="formatMessage(note || '')"
|
|
||||||
class="mt-4 note__content"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
// For RTL direction view
|
|
||||||
.app-rtl--wrapper {
|
|
||||||
.note__content {
|
|
||||||
::v-deep {
|
|
||||||
p {
|
|
||||||
unicode-bidi: plaintext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-wrap:hover {
|
|
||||||
.actions {
|
|
||||||
@apply visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<script>
|
|
||||||
import AddNote from './AddNote.vue';
|
|
||||||
import ContactNote from './ContactNote.vue';
|
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
AddNote,
|
|
||||||
ContactNote,
|
|
||||||
Spinner,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
notes: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
isFetching: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['add', 'edit', 'delete'],
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onAddNote(value) {
|
|
||||||
this.$emit('add', value);
|
|
||||||
},
|
|
||||||
onEditNote(value) {
|
|
||||||
this.$emit('edit', value);
|
|
||||||
},
|
|
||||||
onDeleteNote(value) {
|
|
||||||
this.$emit('delete', value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<AddNote @add="onAddNote" />
|
|
||||||
<ContactNote
|
|
||||||
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-4 text-base">
|
|
||||||
<Spinner size="" />
|
|
||||||
<span>{{ $t('NOTES.FETCHING_NOTES') }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!notes.length" class="text-center p-4 text-base">
|
|
||||||
<span>{{ $t('NOTES.NOT_AVAILABLE') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
options: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['add'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
content: '',
|
|
||||||
date: '',
|
|
||||||
label: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
buttonDisabled() {
|
|
||||||
return this.content && this.date === '';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
resetValue() {
|
|
||||||
this.content = '';
|
|
||||||
this.date = '';
|
|
||||||
},
|
|
||||||
|
|
||||||
optionSelected(event) {
|
|
||||||
this.label = event.target.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
onAdd() {
|
|
||||||
const task = {
|
|
||||||
content: this.content,
|
|
||||||
date: this.date,
|
|
||||||
label: this.label,
|
|
||||||
};
|
|
||||||
this.$emit('add', task);
|
|
||||||
this.resetValue();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="wrap">
|
|
||||||
<div class="input-select-wrap">
|
|
||||||
<textarea
|
|
||||||
v-model="content"
|
|
||||||
class="input--reminder"
|
|
||||||
@keydown.enter.shift.exact="onAdd"
|
|
||||||
/>
|
|
||||||
<div class="select-wrap">
|
|
||||||
<div class="select">
|
|
||||||
<div class="input-group">
|
|
||||||
<i class="ion-android-calendar input-group-label" />
|
|
||||||
<input
|
|
||||||
v-model="date"
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('REMINDER.FOOTER.DUE_DATE')"
|
|
||||||
class="input-group-field"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="task-wrap">
|
|
||||||
<select class="task__type" @change="optionSelected($event)">
|
|
||||||
<option value="" disabled selected>
|
|
||||||
{{ $t('REMINDER.FOOTER.LABEL_TITLE') }}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
v-for="option in options"
|
|
||||||
:key="option.id"
|
|
||||||
:value="option.id"
|
|
||||||
>
|
|
||||||
{{ option.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<woot-button
|
|
||||||
size="tiny"
|
|
||||||
color-scheme="primary"
|
|
||||||
class-names="add-button"
|
|
||||||
:title="$t('REMINDER.ADD_BUTTON.TITLE')"
|
|
||||||
:is-disabled="buttonDisabled"
|
|
||||||
@click="onAdd"
|
|
||||||
>
|
|
||||||
{{ $t('REMINDER.ADD_BUTTON.BUTTON') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.wrap {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: var(--space-smaller);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.input-select-wrap {
|
|
||||||
padding: var(--space-small) var(--space-small);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.input--reminder {
|
|
||||||
font-size: var(--font-size-mini);
|
|
||||||
margin-bottom: var(--space-small);
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-wrap {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.select {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group {
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-size: var(--font-size-mini);
|
|
||||||
|
|
||||||
.input-group-field {
|
|
||||||
height: var(--space-medium);
|
|
||||||
font-size: var(--font-size-micro);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-wrap {
|
|
||||||
.task__type {
|
|
||||||
margin: 0 0 0 var(--space-smaller);
|
|
||||||
height: var(--space-medium);
|
|
||||||
width: fit-content;
|
|
||||||
padding: 0 var(--space-two) 0 var(--space-smaller);
|
|
||||||
font-size: var(--font-size-micro);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
<script>
|
|
||||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
|
||||||
import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations.vue';
|
|
||||||
import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo.vue';
|
|
||||||
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
|
|
||||||
import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
|
|
||||||
import Draggable from 'vuedraggable';
|
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
AccordionItem,
|
|
||||||
ContactConversations,
|
|
||||||
ContactInfo,
|
|
||||||
ContactLabel,
|
|
||||||
CustomAttributes,
|
|
||||||
Draggable,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
contact: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
onClose: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
showAvatar: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showCloseButton: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const {
|
|
||||||
updateUISettings,
|
|
||||||
isContactSidebarItemOpen,
|
|
||||||
contactSidebarItemsOrder,
|
|
||||||
toggleSidebarUIState,
|
|
||||||
} = useUISettings();
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateUISettings,
|
|
||||||
isContactSidebarItemOpen,
|
|
||||||
contactSidebarItemsOrder,
|
|
||||||
toggleSidebarUIState,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
dragEnabled: true,
|
|
||||||
contactSidebarItems: [],
|
|
||||||
dragging: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
hasContactAttributes() {
|
|
||||||
const { custom_attributes: customAttributes } = this.contact;
|
|
||||||
return customAttributes && Object.keys(customAttributes).length;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.contactSidebarItems = this.contactSidebarItemsOrder;
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onDragEnd() {
|
|
||||||
this.dragging = false;
|
|
||||||
this.updateUISettings({
|
|
||||||
contact_sidebar_items_order: this.contactSidebarItems,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="relative w-1/4 h-full min-h-screen overflow-y-auto text-sm bg-white dark:bg-slate-900 border-slate-50 dark:border-slate-800/50"
|
|
||||||
:class="showAvatar ? 'border-l border-solid ' : 'border-r border-solid'"
|
|
||||||
>
|
|
||||||
<ContactInfo
|
|
||||||
:show-close-button="showCloseButton"
|
|
||||||
:show-avatar="showAvatar"
|
|
||||||
:contact="contact"
|
|
||||||
close-icon-name="dismiss"
|
|
||||||
@panel-close="onClose"
|
|
||||||
@toggle-panel="onClose"
|
|
||||||
/>
|
|
||||||
<div class="list-group">
|
|
||||||
<Draggable
|
|
||||||
:list="contactSidebarItems"
|
|
||||||
:disabled="!dragEnabled"
|
|
||||||
ghost-class="ghost"
|
|
||||||
item-key="name"
|
|
||||||
@start="dragging = true"
|
|
||||||
@end="onDragEnd"
|
|
||||||
>
|
|
||||||
<template #item="{ element }">
|
|
||||||
<div :key="element.name" class="list-group-item">
|
|
||||||
<div v-if="element.name === 'contact_attributes'">
|
|
||||||
<AccordionItem
|
|
||||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
|
||||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
|
||||||
compact
|
|
||||||
@toggle="
|
|
||||||
value => toggleSidebarUIState('is_ct_custom_attr_open', value)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<CustomAttributes
|
|
||||||
:contact-id="contact.id"
|
|
||||||
attribute-type="contact_attribute"
|
|
||||||
attribute-from="contact_panel"
|
|
||||||
:custom-attributes="contact.custom_attributes"
|
|
||||||
:empty-state-message="
|
|
||||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.NO_RECORDS_FOUND')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</AccordionItem>
|
|
||||||
</div>
|
|
||||||
<div v-if="element.name === 'contact_labels'">
|
|
||||||
<AccordionItem
|
|
||||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CONTACT_LABELS')"
|
|
||||||
:is-open="isContactSidebarItemOpen('is_ct_labels_open')"
|
|
||||||
@toggle="
|
|
||||||
value => toggleSidebarUIState('is_ct_labels_open', value)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ContactLabel :contact-id="contact.id" class="contact-labels" />
|
|
||||||
</AccordionItem>
|
|
||||||
</div>
|
|
||||||
<div v-if="element.name === 'previous_conversation'">
|
|
||||||
<AccordionItem
|
|
||||||
:title="
|
|
||||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.PREVIOUS_CONVERSATIONS')
|
|
||||||
"
|
|
||||||
:is-open="isContactSidebarItemOpen('is_ct_prev_conv_open')"
|
|
||||||
compact
|
|
||||||
@toggle="
|
|
||||||
value => toggleSidebarUIState('is_ct_prev_conv_open', value)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ContactConversations
|
|
||||||
v-if="contact.id"
|
|
||||||
:contact-id="contact.id"
|
|
||||||
conversation-id=""
|
|
||||||
/>
|
|
||||||
</AccordionItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Draggable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
::v-deep {
|
|
||||||
.contact--profile {
|
|
||||||
@apply pb-3 mb-4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group {
|
|
||||||
.list-group-item {
|
|
||||||
@apply bg-white dark:bg-slate-900;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation--details {
|
|
||||||
@apply py-0 px-4;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
|
||||||
import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { LabelSelector },
|
|
||||||
props: {
|
|
||||||
contactId: {
|
|
||||||
type: [String, Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
savedLabels() {
|
|
||||||
const availableContactLabels = this.$store.getters[
|
|
||||||
'contactLabels/getContactLabels'
|
|
||||||
](this.contactId);
|
|
||||||
return this.allLabels.filter(({ title }) =>
|
|
||||||
availableContactLabels.includes(title)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
...mapGetters({
|
|
||||||
allLabels: 'labels/getLabels',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
contactId(newContactId, prevContactId) {
|
|
||||||
if (newContactId && newContactId !== prevContactId) {
|
|
||||||
this.fetchLabels(newContactId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
const { contactId } = this;
|
|
||||||
this.fetchLabels(contactId);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async onUpdateLabels(selectedLabels) {
|
|
||||||
try {
|
|
||||||
await this.$store.dispatch('contactLabels/update', {
|
|
||||||
contactId: this.contactId,
|
|
||||||
labels: selectedLabels,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
useAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addItem(value) {
|
|
||||||
const result = this.savedLabels.map(item => item.title);
|
|
||||||
result.push(value.title);
|
|
||||||
this.onUpdateLabels(result);
|
|
||||||
},
|
|
||||||
|
|
||||||
removeItem(value) {
|
|
||||||
const result = this.savedLabels
|
|
||||||
.map(label => label.title)
|
|
||||||
.filter(label => label !== value);
|
|
||||||
this.onUpdateLabels(result);
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchLabels(contactId) {
|
|
||||||
if (!contactId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$store.dispatch('contactLabels/get', contactId);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<LabelSelector
|
|
||||||
:all-labels="allLabels"
|
|
||||||
:saved-labels="savedLabels"
|
|
||||||
@add="addItem"
|
|
||||||
@remove="removeItem"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
|
||||||
import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue';
|
|
||||||
import countries from 'shared/constants/countries.js';
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { filterAttributeGroups } from '../contactFilterItems';
|
|
||||||
import { useFilter } from 'shared/composables/useFilter';
|
|
||||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
|
||||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
|
||||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
|
||||||
import { useTrack } from 'dashboard/composables';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
FilterInputBox,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
onClose: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
initialFilterTypes: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
initialAppliedFilters: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
isSegmentsView: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
activeSegmentName: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['applyFilter', 'clearFilters', 'updateSegment'],
|
|
||||||
setup() {
|
|
||||||
const { setFilterAttributes } = useFilter({
|
|
||||||
filteri18nKey: 'CONTACTS_FILTER',
|
|
||||||
attributeModel: 'contact_attribute',
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
setFilterAttributes,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
show: true,
|
|
||||||
appliedFilters: this.initialAppliedFilters,
|
|
||||||
activeSegmentNewName: this.activeSegmentName,
|
|
||||||
filterTypes: this.initialFilterTypes,
|
|
||||||
filterGroups: [],
|
|
||||||
allCustomAttributes: [],
|
|
||||||
filterAttributeGroups,
|
|
||||||
attributeModel: 'contact_attribute',
|
|
||||||
filtersFori18n: 'CONTACTS_FILTER',
|
|
||||||
validationErrors: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
|
||||||
}),
|
|
||||||
hasAppliedFilters() {
|
|
||||||
return this.getAppliedContactFilters.length;
|
|
||||||
},
|
|
||||||
filterModalHeaderTitle() {
|
|
||||||
return !this.isSegmentsView
|
|
||||||
? this.$t('CONTACTS_FILTER.TITLE')
|
|
||||||
: this.$t('CONTACTS_FILTER.EDIT_CUSTOM_SEGMENT');
|
|
||||||
},
|
|
||||||
filterModalSubTitle() {
|
|
||||||
return !this.isSegmentsView
|
|
||||||
? this.$t('CONTACTS_FILTER.SUBTITLE')
|
|
||||||
: this.$t('CONTACTS_FILTER.CUSTOM_VIEWS_SUBTITLE');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const { filterGroups, filterTypes } = this.setFilterAttributes();
|
|
||||||
this.filterTypes = [...this.filterTypes, ...filterTypes];
|
|
||||||
this.filterGroups = filterGroups;
|
|
||||||
|
|
||||||
if (this.getAppliedContactFilters.length && !this.isSegmentsView) {
|
|
||||||
this.appliedFilters = [...this.getAppliedContactFilters];
|
|
||||||
} else if (!this.isSegmentsView) {
|
|
||||||
this.appliedFilters.push({
|
|
||||||
attribute_key: 'name',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: '',
|
|
||||||
query_operator: 'and',
|
|
||||||
attribute_model: 'standard',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getOperatorTypes(key) {
|
|
||||||
switch (key) {
|
|
||||||
case 'list':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
case 'text':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_3;
|
|
||||||
case 'number':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
case 'link':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
case 'date':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_4;
|
|
||||||
case 'checkbox':
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
default:
|
|
||||||
return OPERATORS.OPERATOR_TYPES_1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
customAttributeInputType(key) {
|
|
||||||
switch (key) {
|
|
||||||
case 'date':
|
|
||||||
return 'date';
|
|
||||||
case 'text':
|
|
||||||
return 'plain_text';
|
|
||||||
case 'list':
|
|
||||||
return 'search_select';
|
|
||||||
case 'checkbox':
|
|
||||||
return 'search_select';
|
|
||||||
default:
|
|
||||||
return 'plain_text';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getAttributeModel(key) {
|
|
||||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
|
||||||
return type.attributeModel;
|
|
||||||
},
|
|
||||||
getInputType(key, operator) {
|
|
||||||
if (key === 'created_at' || key === 'last_activity_at')
|
|
||||||
if (operator === 'days_before') return 'plain_text';
|
|
||||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
|
||||||
return type?.inputType;
|
|
||||||
},
|
|
||||||
getOperators(key) {
|
|
||||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
|
||||||
return type?.filterOperators;
|
|
||||||
},
|
|
||||||
getDropdownValues(type) {
|
|
||||||
const allCustomAttributes = this.$store.getters[
|
|
||||||
'attributes/getAttributesByModel'
|
|
||||||
](this.attributeModel);
|
|
||||||
const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
|
|
||||||
return (
|
|
||||||
attr.attribute_key === type &&
|
|
||||||
attr.attribute_display_type === 'checkbox'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (isCustomAttributeCheckbox || type === 'blocked') {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: true,
|
|
||||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: false,
|
|
||||||
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCustomAttributeList = allCustomAttributes.find(attr => {
|
|
||||||
return (
|
|
||||||
attr.attribute_key === type && attr.attribute_display_type === 'list'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isCustomAttributeList) {
|
|
||||||
return allCustomAttributes
|
|
||||||
.find(attr => attr.attribute_key === type)
|
|
||||||
.attribute_values.map(item => {
|
|
||||||
return {
|
|
||||||
id: item,
|
|
||||||
name: item,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
switch (type) {
|
|
||||||
case 'country_code':
|
|
||||||
return countries;
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
appendNewFilter() {
|
|
||||||
if (this.isSegmentsView) {
|
|
||||||
this.setQueryOperatorOnLastQuery();
|
|
||||||
} else {
|
|
||||||
this.appliedFilters.push({
|
|
||||||
attribute_key: 'name',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: '',
|
|
||||||
query_operator: 'and',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setQueryOperatorOnLastQuery() {
|
|
||||||
const lastItemIndex = this.appliedFilters.length - 1;
|
|
||||||
this.appliedFilters[lastItemIndex] = {
|
|
||||||
...this.appliedFilters[lastItemIndex],
|
|
||||||
query_operator: 'and',
|
|
||||||
};
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.appliedFilters.push({
|
|
||||||
attribute_key: 'name',
|
|
||||||
filter_operator: 'equal_to',
|
|
||||||
values: '',
|
|
||||||
query_operator: 'and',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
removeFilter(index) {
|
|
||||||
if (this.appliedFilters.length <= 1) {
|
|
||||||
useAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
|
|
||||||
} else {
|
|
||||||
this.appliedFilters.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submitFilterQuery() {
|
|
||||||
this.validationErrors = validateConversationOrContactFilters(
|
|
||||||
this.appliedFilters
|
|
||||||
);
|
|
||||||
if (Object.keys(this.validationErrors).length === 0) {
|
|
||||||
this.$store.dispatch(
|
|
||||||
'contacts/setContactFilters',
|
|
||||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
|
||||||
);
|
|
||||||
this.$emit('applyFilter', this.appliedFilters);
|
|
||||||
useTrack(CONTACTS_EVENTS.APPLY_FILTER, {
|
|
||||||
applied_filters: this.appliedFilters.map(filter => ({
|
|
||||||
key: filter.attribute_key,
|
|
||||||
operator: filter.filter_operator,
|
|
||||||
query_operator: filter.query_operator,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateSegment() {
|
|
||||||
this.$emit(
|
|
||||||
'updateSegment',
|
|
||||||
this.appliedFilters,
|
|
||||||
this.activeSegmentNewName
|
|
||||||
);
|
|
||||||
},
|
|
||||||
resetFilter(index, currentFilter) {
|
|
||||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
|
||||||
filter => filter.attributeKey === currentFilter.attribute_key
|
|
||||||
).filterOperators[0].value;
|
|
||||||
this.appliedFilters[index].values = '';
|
|
||||||
},
|
|
||||||
showUserInput(operatorType) {
|
|
||||||
if (operatorType === 'is_present' || operatorType === 'is_not_present')
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
clearFilters() {
|
|
||||||
this.$emit('clearFilters');
|
|
||||||
this.onClose();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
|
||||||
<p class="text-slate-600 dark:text-slate-200">
|
|
||||||
{{ filterModalSubTitle }}
|
|
||||||
</p>
|
|
||||||
</woot-modal-header>
|
|
||||||
<div class="p-8">
|
|
||||||
<div v-if="isSegmentsView">
|
|
||||||
<label class="input-label" :class="{ error: !activeSegmentNewName }">
|
|
||||||
{{ $t('CONTACTS_FILTER.SEGMENT_LABEL') }}
|
|
||||||
<input
|
|
||||||
v-model="activeSegmentNewName"
|
|
||||||
type="text"
|
|
||||||
class="bg-white folder-input border-slate-75 dark:border-slate-600 dark:bg-slate-900 text-slate-900 dark:text-slate-100"
|
|
||||||
/>
|
|
||||||
<span v-if="!activeSegmentNewName" class="message">
|
|
||||||
{{ $t('CONTACTS_FILTER.EMPTY_VALUE_ERROR') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<label class="input-label">
|
|
||||||
{{ $t('CONTACTS_FILTER.SEGMENT_QUERY_LABEL') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="p-4 mb-4 border border-solid rounded-lg bg-slate-25 dark:bg-slate-900 border-slate-50 dark:border-slate-700/50"
|
|
||||||
>
|
|
||||||
<FilterInputBox
|
|
||||||
v-for="(filter, i) in appliedFilters"
|
|
||||||
:key="i"
|
|
||||||
v-model="appliedFilters[i]"
|
|
||||||
:filter-groups="filterGroups"
|
|
||||||
grouped-filters
|
|
||||||
:input-type="
|
|
||||||
getInputType(
|
|
||||||
appliedFilters[i].attribute_key,
|
|
||||||
appliedFilters[i].filter_operator
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:operators="getOperators(appliedFilters[i].attribute_key)"
|
|
||||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
|
||||||
:show-query-operator="i !== appliedFilters.length - 1"
|
|
||||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
|
||||||
:error-message="
|
|
||||||
validationErrors[`filter_${i}`]
|
|
||||||
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
@reset-filter="resetFilter(i, appliedFilters[i])"
|
|
||||||
@remove-filter="removeFilter(i)"
|
|
||||||
/>
|
|
||||||
<div class="flex items-center gap-2 mt-4">
|
|
||||||
<woot-button
|
|
||||||
icon="add"
|
|
||||||
color-scheme="success"
|
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
@click="appendNewFilter"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_FILTER.ADD_NEW_FILTER') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
v-if="hasAppliedFilters && !isSegmentsView"
|
|
||||||
icon="subtract"
|
|
||||||
color-scheme="alert"
|
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
@click="clearFilters"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_FILTER.CLEAR_ALL_FILTERS') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
|
||||||
<woot-button class="button clear" @click.prevent="onClose">
|
|
||||||
{{ $t('CONTACTS_FILTER.CANCEL_BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
v-if="isSegmentsView"
|
|
||||||
:disabled="!activeSegmentNewName"
|
|
||||||
@click="updateSegment"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_FILTER.UPDATE_BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button v-else @click="submitFilterQuery">
|
|
||||||
{{ $t('CONTACTS_FILTER.SUBMIT_BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.folder-input {
|
|
||||||
@apply w-[50%];
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { h, ref, computed, defineEmits } from 'vue';
|
|
||||||
import {
|
|
||||||
useVueTable,
|
|
||||||
createColumnHelper,
|
|
||||||
getCoreRowModel,
|
|
||||||
} from '@tanstack/vue-table';
|
|
||||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
|
||||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
|
||||||
|
|
||||||
// Table components
|
|
||||||
import Table from 'dashboard/components/table/Table.vue';
|
|
||||||
import NameCell from './ContactsTable/NameCell.vue';
|
|
||||||
import EmailCell from './ContactsTable/EmailCell.vue';
|
|
||||||
import TelCell from './ContactsTable/TelCell.vue';
|
|
||||||
import CountryCell from './ContactsTable/CountryCell.vue';
|
|
||||||
import ProfilesCell from './ContactsTable/ProfilesCell.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
contacts: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
showSearchEmptyState: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['onSortChange']);
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const tableData = computed(() => {
|
|
||||||
if (props.isLoading) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.contacts.map(item => {
|
|
||||||
// Note: The attributes used here is in snake case
|
|
||||||
// as it simplier the sort attribute calculation
|
|
||||||
const additional = item.additional_attributes || {};
|
|
||||||
const { last_activity_at: lastActivityAt } = item;
|
|
||||||
const { created_at: createdAt } = item;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
profiles: additional.social_profiles || {},
|
|
||||||
last_activity_at: lastActivityAt ? dynamicTime(lastActivityAt) : null,
|
|
||||||
created_at: createdAt ? dynamicTime(createdAt) : null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaulSpanRender = cellProps =>
|
|
||||||
h(
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class: cellProps.getValue() ? '' : 'text-slate-300 dark:text-slate-700',
|
|
||||||
},
|
|
||||||
cellProps.getValue() ? cellProps.getValue() : '---'
|
|
||||||
);
|
|
||||||
|
|
||||||
const columnHelper = createColumnHelper();
|
|
||||||
const columns = [
|
|
||||||
columnHelper.accessor('name', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.NAME'),
|
|
||||||
cell: cellProps => h(NameCell, cellProps),
|
|
||||||
size: 250,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('email', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.EMAIL_ADDRESS'),
|
|
||||||
cell: cellProps => h(EmailCell, { email: cellProps.getValue() }),
|
|
||||||
size: 250,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('phone_number', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.PHONE_NUMBER'),
|
|
||||||
size: 200,
|
|
||||||
cell: cellProps =>
|
|
||||||
h(TelCell, {
|
|
||||||
phoneNumber: cellProps.getValue(),
|
|
||||||
defaultCountry: cellProps.row.original.country_code,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('company_name', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.COMPANY'),
|
|
||||||
size: 200,
|
|
||||||
cell: defaulSpanRender,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('city', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.CITY'),
|
|
||||||
cell: defaulSpanRender,
|
|
||||||
size: 200,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('country', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.COUNTRY'),
|
|
||||||
size: 200,
|
|
||||||
cell: cellProps =>
|
|
||||||
h(CountryCell, {
|
|
||||||
countryCode: cellProps.row.original.country_code,
|
|
||||||
country: cellProps.getValue(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('profiles', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES'),
|
|
||||||
size: 200,
|
|
||||||
enableSorting: false,
|
|
||||||
cell: cellProps =>
|
|
||||||
h(ProfilesCell, {
|
|
||||||
profiles: cellProps.getValue(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('last_activity_at', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.LAST_ACTIVITY'),
|
|
||||||
size: 200,
|
|
||||||
cell: defaulSpanRender,
|
|
||||||
}),
|
|
||||||
columnHelper.accessor('created_at', {
|
|
||||||
header: t('CONTACTS_PAGE.LIST.TABLE_HEADER.CREATED_AT'),
|
|
||||||
size: 200,
|
|
||||||
cell: defaulSpanRender,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
// type ColumnSort = {
|
|
||||||
// id: string
|
|
||||||
// desc: boolean
|
|
||||||
// }
|
|
||||||
// type SortingState = ColumnSort[]
|
|
||||||
const sortingState = ref([{ last_activity_at: 'desc' }]);
|
|
||||||
|
|
||||||
const table = useVueTable({
|
|
||||||
get data() {
|
|
||||||
return tableData.value;
|
|
||||||
},
|
|
||||||
columns,
|
|
||||||
enableMultiSort: false,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
state: {
|
|
||||||
get sorting() {
|
|
||||||
return sortingState.value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onSortingChange: updater => {
|
|
||||||
// onSortingChange returns a callback that allows us to trigger the sort when we need
|
|
||||||
// See more docs here: https://tanstack.com/table/latest/docs/api/features/sorting#onsortingchange
|
|
||||||
// IMO, I don't like this API, but it's what we have for now. Would be great if we could just listen the the changes
|
|
||||||
// to the sorting state and emit the event when it changes
|
|
||||||
// But we can easily wrap this later as a separate composable
|
|
||||||
const updatedSortState = updater(sortingState.value);
|
|
||||||
// we pick the first item from the array, as we are not using multi-sorting
|
|
||||||
const [sort] = updatedSortState;
|
|
||||||
|
|
||||||
if (sort) {
|
|
||||||
sortingState.value = updatedSortState;
|
|
||||||
emit('onSortChange', { [sort.id]: sort.desc ? 'desc' : 'asc' });
|
|
||||||
} else {
|
|
||||||
// If the sorting is empty, we reset to the default sorting
|
|
||||||
sortingState.value = [{ last_activity_at: 'desc' }];
|
|
||||||
emit('onSortChange', {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="flex-1 h-full overflow-auto bg-white dark:bg-slate-900">
|
|
||||||
<section class="overflow-x-auto">
|
|
||||||
<Table fixed :table="table" type="compact" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<EmptyState
|
|
||||||
v-if="showSearchEmptyState"
|
|
||||||
:title="$t('CONTACTS_PAGE.LIST.404')"
|
|
||||||
/>
|
|
||||||
<EmptyState
|
|
||||||
v-else-if="!isLoading && !contacts.length"
|
|
||||||
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
|
||||||
/>
|
|
||||||
<div v-if="isLoading" class="flex items-center justify-center text-base">
|
|
||||||
<Spinner />
|
|
||||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { getCountryFlag } from 'dashboard/helper/flag';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
countryCode: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
country: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const countryFlag = computed(() => {
|
|
||||||
if (!props.countryCode) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return getCountryFlag(props.countryCode);
|
|
||||||
});
|
|
||||||
|
|
||||||
const formattedCountryName = computed(() => {
|
|
||||||
if (!props.country) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${countryFlag.value} ${props.country}`;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BaseCell :content="formattedCountryName" />
|
|
||||||
</template>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
|
||||||
defineProps({
|
|
||||||
email: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BaseCell>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer nofollow"
|
|
||||||
class="text-woot-500 dark:text-woot-500"
|
|
||||||
:href="`mailto:${email}`"
|
|
||||||
>
|
|
||||||
{{ email }}
|
|
||||||
</a>
|
|
||||||
</BaseCell>
|
|
||||||
</template>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { inject } from 'vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
row: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const openContactInfoPanel = inject('openContactInfoPanel');
|
|
||||||
const isRTL = useMapGetter('accounts/isRTL');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BaseCell>
|
|
||||||
<woot-button
|
|
||||||
variant="clear"
|
|
||||||
class="!px-0"
|
|
||||||
@click="() => openContactInfoPanel(row.original.id)"
|
|
||||||
>
|
|
||||||
<div class="items-center flex" :class="{ 'flex-row-reverse': isRTL }">
|
|
||||||
<Thumbnail
|
|
||||||
size="32px"
|
|
||||||
:src="row.original.thumbnail"
|
|
||||||
:username="row.original.name"
|
|
||||||
:status="row.original.availability_status"
|
|
||||||
/>
|
|
||||||
<div class="items-start flex flex-col my-0 mx-2">
|
|
||||||
<h6 class="overflow-hidden text-base whitespace-nowrap text-ellipsis">
|
|
||||||
<router-link
|
|
||||||
:to="`/app/accounts/${route.params.accountId}/contacts/${row.original.id}`"
|
|
||||||
class="text-sm font-medium m-0 capitalize"
|
|
||||||
>
|
|
||||||
{{ row.original.name }}
|
|
||||||
</router-link>
|
|
||||||
</h6>
|
|
||||||
<button
|
|
||||||
class="button clear small link text-slate-600 dark:text-slate-200"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_PAGE.LIST.VIEW_DETAILS') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</woot-button>
|
|
||||||
</BaseCell>
|
|
||||||
</template>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
profiles: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredProfiles = computed(() =>
|
|
||||||
Object.keys(props.profiles).filter(profile => props.profiles[profile])
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BaseCell class="flex gap-2 items-center text-slate-300 dark:text-slate-400">
|
|
||||||
<template v-if="filteredProfiles.length">
|
|
||||||
<a
|
|
||||||
v-for="profile in filteredProfiles"
|
|
||||||
:key="profile"
|
|
||||||
:href="`https://${profile}.com/${profiles[profile]}`"
|
|
||||||
target="_blank"
|
|
||||||
class="hover:text-slate-500"
|
|
||||||
rel="noopener noreferrer nofollow"
|
|
||||||
>
|
|
||||||
<FluentIcon class="size-4" :icon="`brand-${profile}`" />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</BaseCell>
|
|
||||||
</template>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import BaseCell from 'dashboard/components/table/BaseCell.vue';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { parsePhoneNumber } from 'libphonenumber-js';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
phoneNumber: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
defaultCountry: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const formattedNumber = computed(() => {
|
|
||||||
if (!props.phoneNumber) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedNumber = parsePhoneNumber(
|
|
||||||
props.phoneNumber,
|
|
||||||
props.defaultCountry
|
|
||||||
);
|
|
||||||
|
|
||||||
if (parsedNumber) {
|
|
||||||
return parsedNumber.formatInternational();
|
|
||||||
}
|
|
||||||
return props.phoneNumber;
|
|
||||||
} catch (error) {
|
|
||||||
return props.phoneNumber;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BaseCell :content="formattedNumber" />
|
|
||||||
</template>
|
|
||||||
@@ -1,475 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
|
||||||
|
|
||||||
import ContactsHeader from './Header.vue';
|
|
||||||
import ContactsTable from './ContactsTable.vue';
|
|
||||||
import ContactInfoPanel from './ContactInfoPanel.vue';
|
|
||||||
import CreateContact from 'dashboard/routes/dashboard/conversation/contact/CreateContact.vue';
|
|
||||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
|
||||||
import ImportContacts from './ImportContacts.vue';
|
|
||||||
import ContactsAdvancedFilters from './ContactsAdvancedFilters.vue';
|
|
||||||
import contactFilterItems from '../contactFilterItems';
|
|
||||||
import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
|
|
||||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
|
|
||||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
|
||||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
|
||||||
import countries from 'shared/constants/countries.js';
|
|
||||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
|
||||||
import { useTrack } from 'dashboard/composables';
|
|
||||||
|
|
||||||
const DEFAULT_PAGE = 1;
|
|
||||||
const FILTER_TYPE_CONTACT = 1;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ContactsHeader,
|
|
||||||
ContactsTable,
|
|
||||||
TableFooter,
|
|
||||||
ContactInfoPanel,
|
|
||||||
CreateContact,
|
|
||||||
ImportContacts,
|
|
||||||
ContactsAdvancedFilters,
|
|
||||||
AddCustomViews,
|
|
||||||
DeleteCustomViews,
|
|
||||||
},
|
|
||||||
provide() {
|
|
||||||
return {
|
|
||||||
openContactInfoPanel: this.openContactInfoPanel,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
label: { type: String, default: '' },
|
|
||||||
segmentsId: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
searchQuery: '',
|
|
||||||
showCreateModal: false,
|
|
||||||
showImportModal: false,
|
|
||||||
selectedContactId: '',
|
|
||||||
sortConfig: { last_activity_at: 'desc' },
|
|
||||||
showFiltersModal: false,
|
|
||||||
contactFilterItems: contactFilterItems.map(filter => ({
|
|
||||||
...filter,
|
|
||||||
attributeName: this.$t(
|
|
||||||
`CONTACTS_FILTER.ATTRIBUTES.${filter.attributeI18nKey}`
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
segmentsQuery: {},
|
|
||||||
filterType: FILTER_TYPE_CONTACT,
|
|
||||||
showAddSegmentsModal: false,
|
|
||||||
showDeleteSegmentsModal: false,
|
|
||||||
appliedFilter: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
records: 'contacts/getContacts',
|
|
||||||
uiFlags: 'contacts/getUIFlags',
|
|
||||||
meta: 'contacts/getMeta',
|
|
||||||
segments: 'customViews/getContactCustomViews',
|
|
||||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
|
||||||
}),
|
|
||||||
showEmptySearchResult() {
|
|
||||||
const hasEmptyResults = !!this.searchQuery && this.records.length === 0;
|
|
||||||
return hasEmptyResults;
|
|
||||||
},
|
|
||||||
hasAppliedFilters() {
|
|
||||||
return this.getAppliedContactFilters.length;
|
|
||||||
},
|
|
||||||
hasActiveSegments() {
|
|
||||||
return this.activeSegment && this.segmentsId !== 0;
|
|
||||||
},
|
|
||||||
isContactAndLabelDashboard() {
|
|
||||||
return (
|
|
||||||
this.$route.name === 'contacts_dashboard' ||
|
|
||||||
this.$route.name === 'contacts_labels_dashboard'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
pageTitle() {
|
|
||||||
if (this.hasActiveSegments) {
|
|
||||||
return this.activeSegment.name;
|
|
||||||
}
|
|
||||||
if (this.label) {
|
|
||||||
return `#${this.label}`;
|
|
||||||
}
|
|
||||||
return this.$t('CONTACTS_PAGE.HEADER');
|
|
||||||
},
|
|
||||||
selectedContact() {
|
|
||||||
if (this.selectedContactId) {
|
|
||||||
const contact = this.records.find(
|
|
||||||
item => this.selectedContactId === item.id
|
|
||||||
);
|
|
||||||
return contact;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
showContactViewPane() {
|
|
||||||
return this.selectedContactId !== '';
|
|
||||||
},
|
|
||||||
wrapClass() {
|
|
||||||
return this.showContactViewPane ? 'w-[75%]' : 'w-full';
|
|
||||||
},
|
|
||||||
pageParameter() {
|
|
||||||
const selectedPageNumber = Number(this.$route.query?.page);
|
|
||||||
return !Number.isNaN(selectedPageNumber) &&
|
|
||||||
selectedPageNumber >= DEFAULT_PAGE
|
|
||||||
? selectedPageNumber
|
|
||||||
: DEFAULT_PAGE;
|
|
||||||
},
|
|
||||||
activeSegment() {
|
|
||||||
if (this.segmentsId) {
|
|
||||||
const [firstValue] = this.segments.filter(
|
|
||||||
view => view.id === Number(this.segmentsId)
|
|
||||||
);
|
|
||||||
return firstValue;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
activeSegmentName() {
|
|
||||||
return this.activeSegment?.name;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
label() {
|
|
||||||
this.fetchContacts(DEFAULT_PAGE);
|
|
||||||
if (this.hasAppliedFilters) {
|
|
||||||
this.clearFilters();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
activeSegment() {
|
|
||||||
if (this.hasActiveSegments) {
|
|
||||||
const payload = this.activeSegment.query;
|
|
||||||
this.fetchSavedFilteredContact(payload, DEFAULT_PAGE);
|
|
||||||
}
|
|
||||||
if (this.hasAppliedFilters && this.$route.name === 'contacts_dashboard') {
|
|
||||||
this.fetchFilteredContacts(DEFAULT_PAGE);
|
|
||||||
} else {
|
|
||||||
this.fetchContacts(DEFAULT_PAGE);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fetchContacts(this.pageParameter);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updatePageParam(page) {
|
|
||||||
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
|
|
||||||
},
|
|
||||||
getSortAttribute() {
|
|
||||||
let sortAttr = Object.keys(this.sortConfig).reduce((acc, sortKey) => {
|
|
||||||
const sortOrder = this.sortConfig[sortKey];
|
|
||||||
if (sortOrder) {
|
|
||||||
const sortOrderSign = sortOrder === 'asc' ? '' : '-';
|
|
||||||
return `${sortOrderSign}${sortKey}`;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, '');
|
|
||||||
if (!sortAttr) {
|
|
||||||
this.sortConfig = { last_activity_at: 'desc' };
|
|
||||||
sortAttr = '-last_activity_at';
|
|
||||||
}
|
|
||||||
return sortAttr;
|
|
||||||
},
|
|
||||||
fetchContacts(page) {
|
|
||||||
if (this.isContactAndLabelDashboard) {
|
|
||||||
this.updatePageParam(page);
|
|
||||||
let value = '';
|
|
||||||
if (this.searchQuery.charAt(0) === '+') {
|
|
||||||
value = this.searchQuery.substring(1);
|
|
||||||
} else {
|
|
||||||
value = this.searchQuery;
|
|
||||||
}
|
|
||||||
const requestParams = {
|
|
||||||
page,
|
|
||||||
sortAttr: this.getSortAttribute(),
|
|
||||||
label: this.label,
|
|
||||||
};
|
|
||||||
if (!value) {
|
|
||||||
this.$store.dispatch('contacts/get', requestParams);
|
|
||||||
} else {
|
|
||||||
this.$store.dispatch('contacts/search', {
|
|
||||||
search: encodeURIComponent(value),
|
|
||||||
...requestParams,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchSavedFilteredContact(payload, page) {
|
|
||||||
if (this.hasActiveSegments) {
|
|
||||||
this.updatePageParam(page);
|
|
||||||
this.$store.dispatch('contacts/filter', {
|
|
||||||
queryPayload: payload,
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchFilteredContacts(page) {
|
|
||||||
if (this.hasAppliedFilters) {
|
|
||||||
const payload = this.segmentsQuery;
|
|
||||||
this.updatePageParam(page);
|
|
||||||
this.$store.dispatch('contacts/filter', {
|
|
||||||
queryPayload: payload,
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onInputSearch(event) {
|
|
||||||
const newQuery = event.target.value;
|
|
||||||
const refetchAllContacts = !!this.searchQuery && newQuery === '';
|
|
||||||
this.searchQuery = newQuery;
|
|
||||||
if (refetchAllContacts) {
|
|
||||||
this.fetchContacts(DEFAULT_PAGE);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSearchSubmit() {
|
|
||||||
this.selectedContactId = '';
|
|
||||||
if (this.searchQuery) {
|
|
||||||
this.fetchContacts(DEFAULT_PAGE);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPageChange(page) {
|
|
||||||
this.selectedContactId = '';
|
|
||||||
if (this.segmentsId !== 0) {
|
|
||||||
const payload = this.activeSegment.query;
|
|
||||||
this.fetchSavedFilteredContact(payload, page);
|
|
||||||
}
|
|
||||||
if (this.hasAppliedFilters) {
|
|
||||||
this.fetchFilteredContacts(page);
|
|
||||||
} else {
|
|
||||||
this.fetchContacts(page);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openContactInfoPanel(contactId) {
|
|
||||||
this.selectedContactId = contactId;
|
|
||||||
this.showContactInfoPanelPane = true;
|
|
||||||
},
|
|
||||||
closeContactInfoPanel() {
|
|
||||||
this.selectedContactId = '';
|
|
||||||
this.showContactInfoPanelPane = false;
|
|
||||||
},
|
|
||||||
onToggleCreate() {
|
|
||||||
this.showCreateModal = !this.showCreateModal;
|
|
||||||
},
|
|
||||||
onToggleSaveFilters() {
|
|
||||||
this.showAddSegmentsModal = true;
|
|
||||||
},
|
|
||||||
onCloseAddSegmentsModal() {
|
|
||||||
this.showAddSegmentsModal = false;
|
|
||||||
},
|
|
||||||
onToggleDeleteFilters() {
|
|
||||||
this.showDeleteSegmentsModal = true;
|
|
||||||
},
|
|
||||||
onCloseDeleteSegmentsModal() {
|
|
||||||
this.showDeleteSegmentsModal = false;
|
|
||||||
},
|
|
||||||
onToggleImport() {
|
|
||||||
this.showImportModal = !this.showImportModal;
|
|
||||||
},
|
|
||||||
onSortChange(params) {
|
|
||||||
this.sortConfig = params;
|
|
||||||
this.fetchContacts(this.meta.currentPage);
|
|
||||||
|
|
||||||
const sortBy =
|
|
||||||
Object.entries(params).find(pair => Boolean(pair[1])) || [];
|
|
||||||
|
|
||||||
useTrack(CONTACTS_EVENTS.APPLY_SORT, {
|
|
||||||
appliedOn: sortBy[0],
|
|
||||||
order: sortBy[1],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onToggleFilters() {
|
|
||||||
if (this.hasActiveSegments) {
|
|
||||||
this.initializeSegmentToFilterModal(this.activeSegment);
|
|
||||||
}
|
|
||||||
this.showFiltersModal = true;
|
|
||||||
},
|
|
||||||
closeAdvanceFiltersModal() {
|
|
||||||
this.showFiltersModal = false;
|
|
||||||
this.appliedFilter = [];
|
|
||||||
},
|
|
||||||
onApplyFilter(payload) {
|
|
||||||
this.closeContactInfoPanel();
|
|
||||||
this.segmentsQuery = filterQueryGenerator(payload);
|
|
||||||
this.$store.dispatch('contacts/filter', {
|
|
||||||
queryPayload: filterQueryGenerator(payload),
|
|
||||||
});
|
|
||||||
this.showFiltersModal = false;
|
|
||||||
},
|
|
||||||
onUpdateSegment(payload, segmentName) {
|
|
||||||
const payloadData = {
|
|
||||||
...this.activeSegment,
|
|
||||||
name: segmentName,
|
|
||||||
query: filterQueryGenerator(payload),
|
|
||||||
};
|
|
||||||
this.$store.dispatch('customViews/update', payloadData);
|
|
||||||
this.closeAdvanceFiltersModal();
|
|
||||||
},
|
|
||||||
clearFilters() {
|
|
||||||
this.$store.dispatch('contacts/clearContactFilters');
|
|
||||||
this.fetchContacts(this.pageParameter);
|
|
||||||
},
|
|
||||||
onExportSubmit() {
|
|
||||||
let query = { payload: [] };
|
|
||||||
|
|
||||||
if (this.hasActiveSegments) {
|
|
||||||
query = this.activeSegment.query;
|
|
||||||
} else if (this.hasAppliedFilters) {
|
|
||||||
query = filterQueryGenerator(this.getAppliedContactFilters);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.$store.dispatch('contacts/export', {
|
|
||||||
...query,
|
|
||||||
label: this.label,
|
|
||||||
});
|
|
||||||
useAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
|
||||||
} catch (error) {
|
|
||||||
useAlert(error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setParamsForEditSegmentModal() {
|
|
||||||
// Here we are setting the params for edit segment modal to show the existing values.
|
|
||||||
|
|
||||||
// For custom attributes we get only attribute key.
|
|
||||||
// So we are mapping it to find the input type of the attribute to show in the edit segment modal.
|
|
||||||
const params = {
|
|
||||||
countries: countries,
|
|
||||||
filterTypes: contactFilterItems,
|
|
||||||
allCustomAttributes:
|
|
||||||
this.$store.getters['attributes/getAttributesByModel'](
|
|
||||||
'contact_attribute'
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return params;
|
|
||||||
},
|
|
||||||
initializeSegmentToFilterModal(activeSegment) {
|
|
||||||
// Here we are setting the params for edit segment modal.
|
|
||||||
// To show the existing values. when we click on edit segment button.
|
|
||||||
|
|
||||||
// Here we get the query from the active segment.
|
|
||||||
// And we are mapping the query to the actual values.
|
|
||||||
// To show in the edit segment modal by the help of generateValuesForEditCustomViews helper.
|
|
||||||
const query = activeSegment?.query?.payload;
|
|
||||||
if (!Array.isArray(query)) return;
|
|
||||||
|
|
||||||
this.appliedFilter.push(
|
|
||||||
...query.map(filter => ({
|
|
||||||
attribute_key: filter.attribute_key,
|
|
||||||
attribute_model: filter.attribute_model,
|
|
||||||
filter_operator: filter.filter_operator,
|
|
||||||
values: Array.isArray(filter.values)
|
|
||||||
? generateValuesForEditCustomViews(
|
|
||||||
filter,
|
|
||||||
this.setParamsForEditSegmentModal()
|
|
||||||
)
|
|
||||||
: [],
|
|
||||||
query_operator: filter.query_operator,
|
|
||||||
custom_attribute_type: filter.custom_attribute_type,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
openSavedItemInSegment() {
|
|
||||||
const lastItemInSegments = this.segments[this.segments.length - 1];
|
|
||||||
const lastItemId = lastItemInSegments.id;
|
|
||||||
this.$router.push({
|
|
||||||
name: 'contacts_segments_dashboard',
|
|
||||||
params: { id: lastItemId },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openLastItemAfterDeleteInSegment() {
|
|
||||||
if (this.segments.length > 0) {
|
|
||||||
this.openSavedItemInSegment();
|
|
||||||
} else {
|
|
||||||
this.$router.push({ name: 'contacts_dashboard' });
|
|
||||||
this.fetchContacts(DEFAULT_PAGE);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-row w-full">
|
|
||||||
<div class="flex flex-col h-full" :class="wrapClass">
|
|
||||||
<ContactsHeader
|
|
||||||
:search-query="searchQuery"
|
|
||||||
:header-title="pageTitle"
|
|
||||||
:segments-id="segmentsId"
|
|
||||||
this-selected-contact-id=""
|
|
||||||
@on-input-search="onInputSearch"
|
|
||||||
@on-toggle-create="onToggleCreate"
|
|
||||||
@on-toggle-filter="onToggleFilters"
|
|
||||||
@on-search-submit="onSearchSubmit"
|
|
||||||
@on-toggle-import="onToggleImport"
|
|
||||||
@on-export-submit="onExportSubmit"
|
|
||||||
@on-toggle-save-filter="onToggleSaveFilters"
|
|
||||||
@on-toggle-delete-filter="onToggleDeleteFilters"
|
|
||||||
@on-toggle-edit-filter="onToggleFilters"
|
|
||||||
/>
|
|
||||||
<ContactsTable
|
|
||||||
:contacts="records"
|
|
||||||
:show-search-empty-state="showEmptySearchResult"
|
|
||||||
:is-loading="uiFlags.isFetching"
|
|
||||||
:active-contact-id="selectedContactId"
|
|
||||||
@on-sort-change="onSortChange"
|
|
||||||
/>
|
|
||||||
<TableFooter
|
|
||||||
class="border-t border-slate-75 dark:border-slate-700/50"
|
|
||||||
:current-page="Number(meta.currentPage)"
|
|
||||||
:total-count="meta.count"
|
|
||||||
:page-size="15"
|
|
||||||
@page-change="onPageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AddCustomViews
|
|
||||||
v-if="showAddSegmentsModal"
|
|
||||||
:custom-views-query="segmentsQuery"
|
|
||||||
:filter-type="filterType"
|
|
||||||
:open-last-saved-item="openSavedItemInSegment"
|
|
||||||
@close="onCloseAddSegmentsModal"
|
|
||||||
/>
|
|
||||||
<DeleteCustomViews
|
|
||||||
v-if="showDeleteSegmentsModal"
|
|
||||||
v-model:show="showDeleteSegmentsModal"
|
|
||||||
:active-custom-view="activeSegment"
|
|
||||||
:custom-views-id="segmentsId"
|
|
||||||
:active-filter-type="filterType"
|
|
||||||
:open-last-item-after-delete="openLastItemAfterDeleteInSegment"
|
|
||||||
@close="onCloseDeleteSegmentsModal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ContactInfoPanel
|
|
||||||
v-if="showContactViewPane"
|
|
||||||
:contact="selectedContact"
|
|
||||||
:on-close="closeContactInfoPanel"
|
|
||||||
/>
|
|
||||||
<CreateContact :show="showCreateModal" @cancel="onToggleCreate" />
|
|
||||||
<woot-modal v-model:show="showImportModal" :on-close="onToggleImport">
|
|
||||||
<ImportContacts v-if="showImportModal" :on-close="onToggleImport" />
|
|
||||||
</woot-modal>
|
|
||||||
<woot-modal
|
|
||||||
v-model:show="showFiltersModal"
|
|
||||||
:on-close="closeAdvanceFiltersModal"
|
|
||||||
size="medium"
|
|
||||||
>
|
|
||||||
<ContactsAdvancedFilters
|
|
||||||
v-if="showFiltersModal"
|
|
||||||
:on-close="closeAdvanceFiltersModal"
|
|
||||||
:initial-filter-types="contactFilterItems"
|
|
||||||
:initial-applied-filters="appliedFilter"
|
|
||||||
:active-segment-name="activeSegmentName"
|
|
||||||
:is-segments-view="hasActiveSegments"
|
|
||||||
@apply-filter="onApplyFilter"
|
|
||||||
@update-segment="onUpdateSegment"
|
|
||||||
@clear-filters="clearFilters"
|
|
||||||
/>
|
|
||||||
</woot-modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
headerTitle: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
searchQuery: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
segmentsId: {
|
|
||||||
type: [String, Number],
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: [
|
|
||||||
'onToggleSaveFilter',
|
|
||||||
'onToggleEditFilter',
|
|
||||||
'onToggleDeleteFilter',
|
|
||||||
'onToggleCreate',
|
|
||||||
'onToggleFilter',
|
|
||||||
'onToggleImport',
|
|
||||||
'onExportSubmit',
|
|
||||||
'onSearchSubmit',
|
|
||||||
'onInputSearch',
|
|
||||||
],
|
|
||||||
setup() {
|
|
||||||
const { isAdmin } = useAdmin();
|
|
||||||
return {
|
|
||||||
isAdmin,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showCreateModal: false,
|
|
||||||
showImportModal: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
searchButtonClass() {
|
|
||||||
return this.searchQuery !== ''
|
|
||||||
? 'opacity-100 translate-x-0 visible'
|
|
||||||
: '-translate-x-px opacity-0 invisible';
|
|
||||||
},
|
|
||||||
...mapGetters({
|
|
||||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
|
||||||
}),
|
|
||||||
hasAppliedFilters() {
|
|
||||||
return this.getAppliedContactFilters.length;
|
|
||||||
},
|
|
||||||
hasActiveSegments() {
|
|
||||||
return this.segmentsId !== 0;
|
|
||||||
},
|
|
||||||
exportDescription() {
|
|
||||||
return this.hasAppliedFilters
|
|
||||||
? this.$t('EXPORT_CONTACTS.CONFIRM.FILTERED_MESSAGE')
|
|
||||||
: this.$t('EXPORT_CONTACTS.CONFIRM.MESSAGE');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onToggleSegmentsModal() {
|
|
||||||
this.$emit('onToggleSaveFilter');
|
|
||||||
},
|
|
||||||
onToggleEditSegmentsModal() {
|
|
||||||
this.$emit('onToggleEditFilter');
|
|
||||||
},
|
|
||||||
onToggleDeleteSegmentsModal() {
|
|
||||||
this.$emit('onToggleDeleteFilter');
|
|
||||||
},
|
|
||||||
toggleCreate() {
|
|
||||||
this.$emit('onToggleCreate');
|
|
||||||
},
|
|
||||||
toggleFilter() {
|
|
||||||
this.$emit('onToggleFilter');
|
|
||||||
},
|
|
||||||
toggleImport() {
|
|
||||||
this.$emit('onToggleImport');
|
|
||||||
},
|
|
||||||
async submitExport() {
|
|
||||||
const ok =
|
|
||||||
await this.$refs.confirmExportContactsDialog.showConfirmation();
|
|
||||||
|
|
||||||
if (ok) {
|
|
||||||
this.$emit('onExportSubmit');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submitSearch() {
|
|
||||||
this.$emit('onSearchSubmit');
|
|
||||||
},
|
|
||||||
inputSearch(event) {
|
|
||||||
this.$emit('onInputSearch', event);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<header
|
|
||||||
class="bg-white border-b dark:bg-slate-900 border-slate-50 dark:border-slate-800"
|
|
||||||
>
|
|
||||||
<div class="flex justify-between w-full px-4 py-2">
|
|
||||||
<div class="flex flex-col items-center w-full gap-2 md:flex-row">
|
|
||||||
<div class="flex justify-between w-full gap-2">
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center max-w-full min-w-[6.25rem]"
|
|
||||||
>
|
|
||||||
<woot-sidemenu-icon />
|
|
||||||
<h1
|
|
||||||
class="m-0 mx-2 my-0 overflow-hidden text-xl text-slate-900 dark:text-slate-100 whitespace-nowrap text-ellipsis"
|
|
||||||
>
|
|
||||||
{{ headerTitle }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="max-w-[400px] min-w-[100px] flex items-center relative mx-2"
|
|
||||||
>
|
|
||||||
<div class="flex items-center absolute h-full left-2.5">
|
|
||||||
<fluent-icon
|
|
||||||
icon="search"
|
|
||||||
class="h-5 text-sm leading-9 text-slate-700 dark:text-slate-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
:placeholder="$t('CONTACTS_PAGE.SEARCH_INPUT_PLACEHOLDER')"
|
|
||||||
class="!pl-9 !pr-[3.75rem] !text-sm !w-full !h-[2.375rem] !m-0 border-slate-100 dark:border-slate-600"
|
|
||||||
:value="searchQuery"
|
|
||||||
@keyup.enter="submitSearch"
|
|
||||||
@input="inputSearch"
|
|
||||||
/>
|
|
||||||
<woot-button
|
|
||||||
:is-loading="false"
|
|
||||||
class="absolute h-8 px-2 py-0 ml-2 transition-transform duration-100 ease-linear clear right-1"
|
|
||||||
:class-names="searchButtonClass"
|
|
||||||
@click="submitSearch"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<div v-if="hasActiveSegments" class="flex gap-2">
|
|
||||||
<woot-button
|
|
||||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
|
||||||
color-scheme="secondary"
|
|
||||||
icon="edit"
|
|
||||||
@click="onToggleEditSegmentsModal"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_EDIT') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
|
||||||
color-scheme="alert"
|
|
||||||
icon="delete"
|
|
||||||
@click="onToggleDeleteSegmentsModal"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_DELETE') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!hasActiveSegments" class="relative">
|
|
||||||
<div
|
|
||||||
v-if="hasAppliedFilters"
|
|
||||||
class="absolute w-2 h-2 rounded-full top-1 right-3 bg-slate-500 dark:bg-slate-500"
|
|
||||||
/>
|
|
||||||
<woot-button
|
|
||||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
|
||||||
color-scheme="secondary"
|
|
||||||
data-testid="create-new-contact"
|
|
||||||
icon="filter"
|
|
||||||
@click="toggleFilter"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<woot-button
|
|
||||||
v-if="hasAppliedFilters && !hasActiveSegments"
|
|
||||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
|
||||||
color-scheme="alert"
|
|
||||||
variant="clear"
|
|
||||||
icon="save"
|
|
||||||
@click="onToggleSegmentsModal"
|
|
||||||
>
|
|
||||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_SAVE') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
|
||||||
color-scheme="success"
|
|
||||||
icon="person-add"
|
|
||||||
data-testid="create-new-contact"
|
|
||||||
@click="toggleCreate"
|
|
||||||
>
|
|
||||||
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
|
|
||||||
<woot-button
|
|
||||||
v-if="isAdmin"
|
|
||||||
color-scheme="info"
|
|
||||||
icon="upload"
|
|
||||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
|
||||||
@click="toggleImport"
|
|
||||||
>
|
|
||||||
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
|
|
||||||
<woot-button
|
|
||||||
v-if="isAdmin"
|
|
||||||
color-scheme="info"
|
|
||||||
icon="download"
|
|
||||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
|
||||||
@click="submitExport"
|
|
||||||
>
|
|
||||||
{{ $t('EXPORT_CONTACTS.BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<woot-confirm-modal
|
|
||||||
ref="confirmExportContactsDialog"
|
|
||||||
:title="$t('EXPORT_CONTACTS.CONFIRM.TITLE')"
|
|
||||||
:description="exportDescription"
|
|
||||||
:confirm-label="$t('EXPORT_CONTACTS.CONFIRM.YES')"
|
|
||||||
:cancel-label="$t('EXPORT_CONTACTS.CONFIRM.NO')"
|
|
||||||
/>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { useAlert } from 'dashboard/composables';
|
|
||||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
|
||||||
import Modal from '../../../../components/Modal.vue';
|
|
||||||
import { useTrack } from 'dashboard/composables';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Modal,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
onClose: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
show: true,
|
|
||||||
file: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
uiFlags: 'contacts/getUIFlags',
|
|
||||||
}),
|
|
||||||
csvUrl() {
|
|
||||||
return '/downloads/import-contacts-sample.csv';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
useTrack(CONTACTS_EVENTS.IMPORT_MODAL_OPEN);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async uploadFile() {
|
|
||||||
try {
|
|
||||||
if (!this.file) return;
|
|
||||||
await this.$store.dispatch('contacts/import', this.file);
|
|
||||||
this.onClose();
|
|
||||||
useAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
|
||||||
useTrack(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
|
||||||
} catch (error) {
|
|
||||||
useAlert(error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE'));
|
|
||||||
useTrack(CONTACTS_EVENTS.IMPORT_FAILURE);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleFileUpload() {
|
|
||||||
this.file = this.$refs.file.files[0];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal v-model:show="show" :on-close="onClose">
|
|
||||||
<div class="flex flex-col h-auto overflow-auto">
|
|
||||||
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
|
|
||||||
<p>
|
|
||||||
{{ $t('IMPORT_CONTACTS.DESC') }}
|
|
||||||
<a :href="csvUrl" download="import-contacts-sample">{{
|
|
||||||
$t('IMPORT_CONTACTS.DOWNLOAD_LABEL')
|
|
||||||
}}</a>
|
|
||||||
</p>
|
|
||||||
</woot-modal-header>
|
|
||||||
<div class="flex flex-col p-8">
|
|
||||||
<div class="w-full">
|
|
||||||
<label>
|
|
||||||
<span>{{ $t('IMPORT_CONTACTS.FORM.LABEL') }}</span>
|
|
||||||
<input
|
|
||||||
id="file"
|
|
||||||
ref="file"
|
|
||||||
type="file"
|
|
||||||
accept="text/csv"
|
|
||||||
@change="handleFileUpload"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
|
||||||
<div class="w-full">
|
|
||||||
<woot-button
|
|
||||||
:disabled="uiFlags.isCreating || !file"
|
|
||||||
:loading="uiFlags.isCreating"
|
|
||||||
@click="uploadFile"
|
|
||||||
>
|
|
||||||
{{ $t('IMPORT_CONTACTS.FORM.SUBMIT') }}
|
|
||||||
</woot-button>
|
|
||||||
<button class="button clear" @click.prevent="onClose">
|
|
||||||
{{ $t('IMPORT_CONTACTS.FORM.CANCEL') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
id: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
isCompleted: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['completed', 'edit', 'delete'],
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onClick() {
|
|
||||||
this.$emit('completed', this.isCompleted);
|
|
||||||
},
|
|
||||||
onEdit() {
|
|
||||||
this.$emit('edit', this.id);
|
|
||||||
},
|
|
||||||
onDelete() {
|
|
||||||
this.$emit('delete', this.id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="reminder-wrap">
|
|
||||||
<div class="status-wrap">
|
|
||||||
<input :checked="isCompleted" type="radio" @click="onClick" />
|
|
||||||
</div>
|
|
||||||
<div class="wrap">
|
|
||||||
<p class="content">
|
|
||||||
{{ text }}
|
|
||||||
</p>
|
|
||||||
<div class="footer">
|
|
||||||
<div class="meta">
|
|
||||||
<woot-label
|
|
||||||
:title="date"
|
|
||||||
description="date"
|
|
||||||
icon="ion-android-calendar"
|
|
||||||
color-scheme="secondary"
|
|
||||||
/>
|
|
||||||
<woot-label
|
|
||||||
:title="label"
|
|
||||||
description="label"
|
|
||||||
color-scheme="secondary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<woot-button
|
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
icon="edit"
|
|
||||||
color-scheme="secondary"
|
|
||||||
class="action-button"
|
|
||||||
@click="onEdit"
|
|
||||||
/>
|
|
||||||
<woot-button
|
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
icon="ion-trash-b"
|
|
||||||
color-scheme="secondary"
|
|
||||||
class="action-button"
|
|
||||||
@click="onDelete"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.reminder-wrap {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: var(--space-smaller);
|
|
||||||
|
|
||||||
.status-wrap {
|
|
||||||
padding: var(--space-small) var(--space-smaller);
|
|
||||||
margin-top: var(--space-smaller);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap {
|
|
||||||
padding: var(--space-small);
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
margin-right: var(--space-small);
|
|
||||||
height: var(--space-medium);
|
|
||||||
width: var(--space-medium);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reminder-wrap:hover {
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
emits: ['notes', 'events', 'conversation'],
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onClickNotes() {
|
|
||||||
this.$emit('notes');
|
|
||||||
},
|
|
||||||
|
|
||||||
onClickEvents() {
|
|
||||||
this.$emit('events');
|
|
||||||
},
|
|
||||||
|
|
||||||
onClickConversation() {
|
|
||||||
this.$emit('conversation');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="wrap">
|
|
||||||
<div class="header">
|
|
||||||
<h5 class="text-lg text-black-900 dark:text-slate-200">
|
|
||||||
{{ $t('EVENTS.HEADER.TITLE') }}
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="button-wrap">
|
|
||||||
<woot-button
|
|
||||||
variant="hollow"
|
|
||||||
size="tiny"
|
|
||||||
color-scheme="secondary"
|
|
||||||
class-names="pill-button"
|
|
||||||
@click="onClickNotes"
|
|
||||||
>
|
|
||||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_NOTES') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
variant="hollow"
|
|
||||||
size="tiny"
|
|
||||||
color-scheme="secondary"
|
|
||||||
class-names="pill-button"
|
|
||||||
@click="onClickEvents"
|
|
||||||
>
|
|
||||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_EVENTS') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
variant="hollow"
|
|
||||||
size="tiny"
|
|
||||||
color-scheme="secondary"
|
|
||||||
class-names="pill-button"
|
|
||||||
@click="onClickConversation"
|
|
||||||
>
|
|
||||||
{{ $t('EVENTS.BUTTON.PILL_BUTTON_CONVO') }}
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.wrap {
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-normal);
|
|
||||||
|
|
||||||
.button-wrap {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.pill-button {
|
|
||||||
margin-right: var(--space-small);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
<!-- Unused file deprecated -->
|
|
||||||
<script>
|
|
||||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
eventType: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
eventPath: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
eventBody: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
timeStamp: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['more'],
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
readableTime() {
|
|
||||||
return dynamicTime(this.timeStamp);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
onClick() {
|
|
||||||
this.$emit('more');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="timeline-card-wrap">
|
|
||||||
<div class="icon-chatbox">
|
|
||||||
<i class="ion-chatboxes" />
|
|
||||||
</div>
|
|
||||||
<div class="card-wrap">
|
|
||||||
<div class="header">
|
|
||||||
<div class="text-wrap">
|
|
||||||
<h6 class="text-sm">
|
|
||||||
{{ eventType }}
|
|
||||||
</h6>
|
|
||||||
<span class="event-path">{{ 'on' }} {{ eventPath }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="date-wrap">
|
|
||||||
<span>{{ readableTime }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="comment-wrap">
|
|
||||||
<p class="comment">
|
|
||||||
{{ eventBody }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="icon-more" @click="onClick">
|
|
||||||
<i class="ion-android-more-vertical" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.timeline-card-wrap {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--color-body);
|
|
||||||
padding: var(--space-small);
|
|
||||||
|
|
||||||
.icon-chatbox {
|
|
||||||
width: var(--space-large);
|
|
||||||
height: var(--space-large);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background-color: var(--color-background);
|
|
||||||
|
|
||||||
.ion-chatboxes {
|
|
||||||
font-size: var(--font-size-default);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-wrap {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-smaller) var(--space-normal) 0;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.text-wrap {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-path {
|
|
||||||
font-size: var(--font-size-mini);
|
|
||||||
margin-left: var(--space-smaller);
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-wrap {
|
|
||||||
font-size: var(--font-size-micro);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-wrap {
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
|
|
||||||
.comment {
|
|
||||||
padding: var(--space-small);
|
|
||||||
font-size: var(--font-size-mini);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-more {
|
|
||||||
.ion-android-more-vertical {
|
|
||||||
font-size: var(--font-size-medium);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { shallowMount } from '@vue/test-utils';
|
|
||||||
import AddReminder from '../AddReminder.vue';
|
|
||||||
|
|
||||||
let wrapper;
|
|
||||||
|
|
||||||
describe('AddReminder', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
wrapper = shallowMount(AddReminder, {
|
|
||||||
mocks: {
|
|
||||||
$t: x => x,
|
|
||||||
$store: { getters: {}, state: {} },
|
|
||||||
},
|
|
||||||
stubs: { WootButton: { template: '<button />' } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tests resetValue', () => {
|
|
||||||
const resetValue = vi.spyOn(wrapper.vm, 'resetValue');
|
|
||||||
wrapper.vm.content = 'test';
|
|
||||||
wrapper.vm.date = '08/11/2022';
|
|
||||||
wrapper.vm.resetValue();
|
|
||||||
expect(wrapper.vm.content).toEqual('');
|
|
||||||
expect(wrapper.vm.date).toEqual('');
|
|
||||||
expect(resetValue).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tests optionSelected', () => {
|
|
||||||
const optionSelected = vi.spyOn(wrapper.vm, 'optionSelected');
|
|
||||||
wrapper.vm.label = '';
|
|
||||||
wrapper.vm.optionSelected({ target: { value: 'test' } });
|
|
||||||
expect(wrapper.vm.label).toEqual('test');
|
|
||||||
expect(optionSelected).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tests onAdd', () => {
|
|
||||||
const onAdd = vi.spyOn(wrapper.vm, 'onAdd');
|
|
||||||
wrapper.vm.label = 'label';
|
|
||||||
wrapper.vm.content = 'content';
|
|
||||||
wrapper.vm.date = '08/11/2022';
|
|
||||||
wrapper.vm.onAdd();
|
|
||||||
expect(onAdd).toHaveBeenCalled();
|
|
||||||
expect(wrapper.emitted().add[0]).toEqual([
|
|
||||||
{ content: 'content', date: '08/11/2022', label: 'label' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<script>
|
|
||||||
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
customAttributes: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
listOfAttributes() {
|
|
||||||
return Object.keys(this.customAttributes).filter(key => {
|
|
||||||
const value = this.customAttributes[key];
|
|
||||||
return value !== null && value !== undefined && value !== '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
valueWithLink(attribute) {
|
|
||||||
const parsedAttribute = this.parseAttributeToString(attribute);
|
|
||||||
const messageFormatter = new MessageFormatter(parsedAttribute);
|
|
||||||
return messageFormatter.formattedMessage;
|
|
||||||
},
|
|
||||||
parseAttributeToString(attribute) {
|
|
||||||
switch (typeof attribute) {
|
|
||||||
case 'string':
|
|
||||||
return attribute;
|
|
||||||
case 'object':
|
|
||||||
return JSON.stringify(attribute);
|
|
||||||
default:
|
|
||||||
return `${attribute}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="custom-attributes--panel">
|
|
||||||
<div
|
|
||||||
v-for="attribute in listOfAttributes"
|
|
||||||
:key="attribute"
|
|
||||||
class="custom-attribute--row"
|
|
||||||
>
|
|
||||||
<div class="custom-attribute--row__attribute">
|
|
||||||
{{ attribute }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span v-dompurify-html="valueWithLink(customAttributes[attribute])" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-if="!listOfAttributes.length">
|
|
||||||
{{ $t('CUSTOM_ATTRIBUTES.NOT_AVAILABLE') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.custom-attributes--panel {
|
|
||||||
margin-bottom: var(--space-normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-attribute--row__attribute {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import ContactForm from './ContactForm.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ContactForm,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['cancel', 'update:show'],
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
uiFlags: 'contacts/getUIFlags',
|
|
||||||
}),
|
|
||||||
localShow: {
|
|
||||||
get() {
|
|
||||||
return this.show;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$emit('update:show', value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onCancel() {
|
|
||||||
this.$emit('cancel');
|
|
||||||
},
|
|
||||||
onSuccess() {
|
|
||||||
this.$emit('cancel');
|
|
||||||
},
|
|
||||||
async onSubmit(contactItem) {
|
|
||||||
await this.$store.dispatch('contacts/create', contactItem);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<woot-modal
|
|
||||||
v-model:show="localShow"
|
|
||||||
:on-close="onCancel"
|
|
||||||
modal-type="right-aligned"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col h-auto overflow-auto">
|
|
||||||
<woot-modal-header
|
|
||||||
:header-title="$t('CREATE_CONTACT.TITLE')"
|
|
||||||
:header-content="$t('CREATE_CONTACT.DESC')"
|
|
||||||
/>
|
|
||||||
<ContactForm
|
|
||||||
:in-progress="uiFlags.isCreating"
|
|
||||||
:on-submit="onSubmit"
|
|
||||||
@success="onSuccess"
|
|
||||||
@cancel="onCancel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</woot-modal>
|
|
||||||
</template>
|
|
||||||
Reference in New Issue
Block a user