chore: Remove unused files in contact (#10570)

This commit is contained in:
Sivin Varghese
2024-12-12 08:59:24 +05:30
committed by GitHub
parent 99c699ea34
commit 86d37622c8
32 changed files with 10 additions and 3803 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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' },
]);
});
});

View File

@@ -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>

View File

@@ -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>