Merge branch 'develop' into snyk-fix-531a03562aa6be95858cdfbccbde51f1
This commit is contained in:
+8
-8
@@ -452,7 +452,7 @@ GEM
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.22.0)
|
||||
loofah (2.23.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -471,7 +471,7 @@ GEM
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.1)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
@@ -502,14 +502,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.7)
|
||||
nokogiri (1.16.8)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-arm64-darwin)
|
||||
nokogiri (1.16.8-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-darwin)
|
||||
nokogiri (1.16.8-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
nokogiri (1.16.8-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
@@ -599,9 +599,9 @@ GEM
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (7.0.8.5)
|
||||
actionpack (= 7.0.8.5)
|
||||
activesupport (= 7.0.8.5)
|
||||
|
||||
@@ -214,6 +214,10 @@ const resetValidation = () => {
|
||||
v$.value.$reset();
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(state, defaultState);
|
||||
};
|
||||
|
||||
watch(() => props.contactData, prepareStateBasedOnProps, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
@@ -224,6 +228,7 @@ defineExpose({
|
||||
state,
|
||||
resetValidation,
|
||||
isFormInvalid,
|
||||
resetForm,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -261,7 +266,7 @@ defineExpose({
|
||||
:placeholder="item.placeholder"
|
||||
:message-type="getMessageType(item.key)"
|
||||
:custom-input-class="`h-8 !pt-1 !pb-1 ${
|
||||
!isDetailsView ? '[&:not(.error)]:!border-transparent' : ''
|
||||
!isDetailsView ? '[&:not(.error,.focus)]:!border-transparent' : ''
|
||||
}`"
|
||||
class="w-full"
|
||||
@input="
|
||||
|
||||
+6
-1
@@ -27,11 +27,16 @@ const handleDialogConfirm = async () => {
|
||||
emit('create', contact.value);
|
||||
};
|
||||
|
||||
const onSuccess = () => {
|
||||
contactsFormRef.value?.resetForm();
|
||||
dialogRef.value.close();
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogRef.value.close();
|
||||
};
|
||||
|
||||
defineExpose({ dialogRef, contactsFormRef });
|
||||
defineExpose({ dialogRef, contactsFormRef, onSuccess });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -54,9 +54,9 @@ const emit = defineEmits([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="sticky top-0 z-10 px-6 xl:px-0">
|
||||
<header class="sticky top-0 z-10">
|
||||
<div
|
||||
class="flex items-center justify-between w-full h-20 gap-2 mx-auto max-w-[960px]"
|
||||
class="flex items-center justify-between w-full h-20 px-6 gap-2 mx-auto max-w-[960px]"
|
||||
>
|
||||
<span class="text-xl font-medium truncate text-n-slate-12">
|
||||
{{ headerTitle }}
|
||||
|
||||
+41
-36
@@ -7,6 +7,10 @@ import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { CONTACTS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
||||
import contactFilterItems from 'dashboard/routes/dashboard/contacts/contactFilterItems';
|
||||
import {
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
import countries from 'shared/constants/countries';
|
||||
import {
|
||||
@@ -23,38 +27,14 @@ import DeleteSegmentDialog from 'dashboard/components-next/Contacts/ContactsForm
|
||||
import ContactsFilter from 'dashboard/components-next/filter/ContactsFilter.vue';
|
||||
|
||||
const props = defineProps({
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeSort: {
|
||||
type: String,
|
||||
default: 'last_activity_at',
|
||||
},
|
||||
activeOrdering: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
segmentsId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
activeSegment: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
hasAppliedFilters: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showSearch: { type: Boolean, default: true },
|
||||
searchValue: { type: String, default: '' },
|
||||
activeSort: { type: String, default: 'last_activity_at' },
|
||||
activeOrdering: { type: String, default: '' },
|
||||
headerTitle: { type: String, default: '' },
|
||||
segmentsId: { type: [String, Number], default: 0 },
|
||||
activeSegment: { type: Object, default: null },
|
||||
hasAppliedFilters: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -99,8 +79,26 @@ const openDeleteSegmentDialog = () =>
|
||||
deleteSegmentDialogRef.value?.dialogRef.open();
|
||||
|
||||
const onCreate = async contact => {
|
||||
await store.dispatch('contacts/create', contact);
|
||||
createNewContactDialogRef.value?.dialogRef.close();
|
||||
try {
|
||||
await store.dispatch('contacts/create', contact);
|
||||
createNewContactDialogRef.value?.onSuccess();
|
||||
useAlert(
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SUCCESS_MESSAGE')
|
||||
);
|
||||
} catch (error) {
|
||||
const i18nPrefix = 'CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION';
|
||||
if (error instanceof DuplicateContactException) {
|
||||
if (error.data.includes('email')) {
|
||||
useAlert(t(`${i18nPrefix}.EMAIL_ADDRESS_DUPLICATE`));
|
||||
} else if (error.data.includes('phone_number')) {
|
||||
useAlert(t(`${i18nPrefix}.PHONE_NUMBER_DUPLICATE`));
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
useAlert(t(`${i18nPrefix}.ERROR_MESSAGE`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onImport = async file => {
|
||||
@@ -135,11 +133,19 @@ const onCreateSegment = async payload => {
|
||||
...payload,
|
||||
query: segmentsQuery.value,
|
||||
};
|
||||
await store.dispatch('customViews/create', payloadData);
|
||||
const response = await store.dispatch('customViews/create', payloadData);
|
||||
createSegmentDialogRef.value?.dialogRef.close();
|
||||
useAlert(
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.FILTERS.CREATE_SEGMENT.SUCCESS_MESSAGE')
|
||||
);
|
||||
const segmentId = response?.data?.id;
|
||||
if (!segmentId) return;
|
||||
// Navigate to the created segment
|
||||
router.push({
|
||||
name: 'contacts_dashboard_segments_index',
|
||||
params: { segmentId },
|
||||
query: { page: 1 },
|
||||
});
|
||||
} catch {
|
||||
useAlert(
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.FILTERS.CREATE_SEGMENT.ERROR_MESSAGE')
|
||||
@@ -176,7 +182,6 @@ const closeAdvanceFiltersModal = () => {
|
||||
};
|
||||
|
||||
const clearFilters = async () => {
|
||||
await store.dispatch('contacts/clearContactFilters');
|
||||
emit('clearFilters');
|
||||
};
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import ActiveFilterPreview from 'dashboard/components-next/filter/ActiveFilterPreview.vue';
|
||||
|
||||
const emit = defineEmits(['clearFilters']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const appliedFilters = useMapGetter('contacts/getAppliedContactFiltersV4');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ActiveFilterPreview
|
||||
:applied-filters="appliedFilters"
|
||||
:max-visible-filters="2"
|
||||
:more-filters-label="
|
||||
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.MORE_FILTERS', {
|
||||
count: appliedFilters.length - 2,
|
||||
})
|
||||
"
|
||||
:clear-button-label="
|
||||
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
|
||||
"
|
||||
class="max-w-[960px] px-6"
|
||||
@clear-filters="emit('clearFilters')"
|
||||
/>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@ import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import ContactListHeaderWrapper from 'dashboard/components-next/Contacts/ContactsHeader/ContactListHeaderWrapper.vue';
|
||||
import ContactsActiveFiltersPreview from 'dashboard/components-next/Contacts/ContactsHeader/components/ContactsActiveFiltersPreview.vue';
|
||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||
|
||||
defineProps({
|
||||
@@ -90,8 +91,12 @@ const updateCurrentPage = page => {
|
||||
@apply-filter="emit('applyFilter', $event)"
|
||||
@clear-filters="emit('clearFilters')"
|
||||
/>
|
||||
<main class="flex-1 px-6 overflow-y-auto xl:px-px">
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<div class="w-full mx-auto max-w-[960px]">
|
||||
<ContactsActiveFiltersPreview
|
||||
v-if="hasAppliedFilters && isNotSegmentView"
|
||||
@clear-filters="emit('clearFilters')"
|
||||
/>
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -58,7 +58,7 @@ const toggleExpanded = id => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<div class="flex flex-col gap-4 px-6 pt-4 pb-6">
|
||||
<ContactsCard
|
||||
v-for="contact in contacts"
|
||||
:id="contact.id"
|
||||
|
||||
@@ -76,6 +76,7 @@ const togglePortalSwitcher = () => {
|
||||
<Button
|
||||
icon="i-lucide-chevron-down"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-slate-3 hover:bg-n-slate-3"
|
||||
@click="togglePortalSwitcher"
|
||||
|
||||
+3
@@ -182,6 +182,7 @@ onMounted(() => {
|
||||
<OnClickOutside @trigger="openAgentsList = false">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
class="!px-0 font-normal hover:!bg-transparent"
|
||||
text-variant="info"
|
||||
@click="openAgentsList = !openAgentsList"
|
||||
@@ -214,6 +215,7 @@ onMounted(() => {
|
||||
"
|
||||
:icon="!selectedCategory?.icon ? 'i-lucide-shapes' : ''"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
class="!px-2 font-normal hover:!bg-transparent"
|
||||
@click="openCategoryList = !openCategoryList"
|
||||
>
|
||||
@@ -244,6 +246,7 @@ onMounted(() => {
|
||||
"
|
||||
icon="i-lucide-plus"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
:disabled="isNewArticle"
|
||||
class="!px-2 font-normal hover:!bg-transparent hover:!text-n-slate-11"
|
||||
@click="openProperties = !openProperties"
|
||||
|
||||
+1
@@ -66,6 +66,7 @@ onMounted(() => {
|
||||
icon="i-lucide-x"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
class="hover:text-n-slate-11"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
|
||||
@@ -108,6 +108,7 @@ const redirectToPortalHomePage = () => {
|
||||
<Button
|
||||
icon="i-lucide-arrow-up-right"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
icon-lib="lucide"
|
||||
size="sm"
|
||||
class="!w-6 !h-6 hover:bg-n-slate-2 text-n-slate-11 !p-0.5 rounded-md"
|
||||
@@ -133,6 +134,7 @@ const redirectToPortalHomePage = () => {
|
||||
:key="index"
|
||||
:label="portal.name"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
trailing-icon
|
||||
:icon="isPortalActive(portal) ? 'i-lucide-check' : ''"
|
||||
class="!justify-end !px-2 !py-2 hover:!bg-n-alpha-2 [&>.i-lucide-check]:text-n-teal-10 h-9"
|
||||
|
||||
@@ -114,8 +114,13 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
<!-- Ghost & Link Variants -->
|
||||
<Variant title="Ghost & Link Variants">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<Button label="Ghost Button" variant="ghost" />
|
||||
<Button label="Ghost with Icon" variant="ghost" icon="i-lucide-plus" />
|
||||
<Button label="Ghost Button" variant="ghost" color="slate" />
|
||||
<Button
|
||||
label="Ghost with Icon"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
icon="i-lucide-plus"
|
||||
/>
|
||||
<Button label="Link Button" variant="link" />
|
||||
<Button label="Link with Icon" variant="link" icon="i-lucide-plus" />
|
||||
</div>
|
||||
|
||||
@@ -88,6 +88,7 @@ const STYLE_CONFIG = {
|
||||
faded:
|
||||
'bg-n-brand/10 text-n-blue-text hover:bg-n-brand/20 outline-transparent',
|
||||
outline: 'text-n-blue-text outline-n-blue-border',
|
||||
ghost: 'text-n-blue-text hover:bg-n-alpha-2 outline-transparent',
|
||||
link: 'text-n-blue-text hover:underline outline-transparent',
|
||||
},
|
||||
ruby: {
|
||||
@@ -95,6 +96,7 @@ const STYLE_CONFIG = {
|
||||
faded:
|
||||
'bg-n-ruby-9/10 text-n-ruby-11 hover:bg-n-ruby-9/20 outline-transparent',
|
||||
outline: 'text-n-ruby-11 hover:bg-n-ruby-9/10 outline-n-ruby-8',
|
||||
ghost: 'text-n-ruby-11 hover:bg-n-alpha-2 outline-transparent',
|
||||
link: 'text-n-ruby-9 hover:underline outline-transparent',
|
||||
},
|
||||
amber: {
|
||||
@@ -103,6 +105,7 @@ const STYLE_CONFIG = {
|
||||
'bg-n-amber-9/10 text-n-slate-12 hover:bg-n-amber-9/20 outline-transparent',
|
||||
outline: 'text-n-amber-11 hover:bg-n-amber-9/10 outline-n-amber-9',
|
||||
link: 'text-n-amber-9 hover:underline outline-transparent',
|
||||
ghost: 'text-n-amber-9 hover:bg-n-alpha-2 outline-transparent',
|
||||
},
|
||||
slate: {
|
||||
solid:
|
||||
@@ -111,6 +114,7 @@ const STYLE_CONFIG = {
|
||||
'bg-n-slate-9/10 text-n-slate-12 hover:bg-n-slate-9/20 outline-transparent',
|
||||
outline: 'text-n-slate-11 outline-n-strong hover:bg-n-slate-9/10',
|
||||
link: 'text-n-slate-11 hover:text-n-slate-12 hover:underline outline-transparent',
|
||||
ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
|
||||
},
|
||||
teal: {
|
||||
solid: 'bg-n-teal-9 text-white hover:bg-n-teal-10 outline-transparent',
|
||||
@@ -118,6 +122,7 @@ const STYLE_CONFIG = {
|
||||
'bg-n-teal-9/10 text-n-slate-12 hover:bg-n-teal-9/20 outline-transparent',
|
||||
outline: 'text-n-teal-11 hover:bg-n-teal-9/10 outline-n-teal-9',
|
||||
link: 'text-n-teal-9 hover:underline outline-transparent',
|
||||
ghost: 'text-n-teal-9 hover:bg-n-alpha-2 outline-transparent',
|
||||
},
|
||||
},
|
||||
sizes: {
|
||||
@@ -151,7 +156,7 @@ const STYLE_CONFIG = {
|
||||
|
||||
const variantClasses = computed(() => {
|
||||
const variantMap = {
|
||||
ghost: 'text-n-slate-12 hover:bg-n-alpha-2 outline-transparent',
|
||||
ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`,
|
||||
link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
|
||||
outline: STYLE_CONFIG.colors[computedColor.value].outline,
|
||||
faded: STYLE_CONFIG.colors[computedColor.value].faded,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { sampleActiveFilters } from './fixtures/filterTypes';
|
||||
import ActiveFilterPreview from './ActiveFilterPreview.vue';
|
||||
|
||||
const appliedFilters = ref([...sampleActiveFilters]);
|
||||
const maxVisibleFilters = ref(2);
|
||||
|
||||
const moreFiltersLabel = computed(
|
||||
() => `${appliedFilters.value.length - maxVisibleFilters.value} more filters`
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Filters/ActiveFilterPreview"
|
||||
:layout="{ type: 'grid', width: '960px' }"
|
||||
>
|
||||
<Variant title="Default (2 visible filters)">
|
||||
<ActiveFilterPreview
|
||||
:applied-filters="appliedFilters.slice(0, 2)"
|
||||
:max-visible-filters="2"
|
||||
clear-button-label="Clear Filters"
|
||||
more-filters-label=""
|
||||
@clear-filters="() => {}"
|
||||
/>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Multiple Filters (with more indicator)">
|
||||
<ActiveFilterPreview
|
||||
:applied-filters="appliedFilters"
|
||||
:max-visible-filters="maxVisibleFilters"
|
||||
clear-button-label="Clear Filters"
|
||||
:more-filters-label="moreFiltersLabel"
|
||||
@clear-filters="() => {}"
|
||||
/>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Show All Filters">
|
||||
<ActiveFilterPreview
|
||||
:applied-filters="appliedFilters"
|
||||
:max-visible-filters="10"
|
||||
clear-button-label="Clear Filters"
|
||||
@clear-filters="() => {}"
|
||||
/>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Single Filter">
|
||||
<ActiveFilterPreview
|
||||
:applied-filters="[appliedFilters[0]]"
|
||||
:max-visible-filters="maxVisibleFilters"
|
||||
clear-button-label="Clear Filters"
|
||||
@clear-filters="() => {}"
|
||||
/>
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Object Values">
|
||||
<ActiveFilterPreview
|
||||
:applied-filters="[appliedFilters[6]]"
|
||||
:max-visible-filters="maxVisibleFilters"
|
||||
clear-button-label="Clear Filters"
|
||||
@clear-filters="() => {}"
|
||||
/>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup>
|
||||
import { replaceUnderscoreWithSpace } from './helper/filterHelper.js';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
appliedFilters: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
maxVisibleFilters: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
clearButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
moreFiltersLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['clearFilters']);
|
||||
|
||||
const shouldCapitalizeFirstLetter = key => {
|
||||
const lowercaseKeys = ['email'];
|
||||
return !lowercaseKeys.includes(key);
|
||||
};
|
||||
|
||||
const formatOperatorLabel = operator => {
|
||||
const operators = {
|
||||
equal_to: 'is',
|
||||
not_equal_to: 'is not',
|
||||
contains: 'contains',
|
||||
does_not_contain: 'does not contain',
|
||||
is_present: 'is present',
|
||||
is_not_present: 'is not present',
|
||||
is_greater_than: 'is greater than',
|
||||
is_less_than: 'is less than',
|
||||
days_before: 'days before',
|
||||
};
|
||||
return operators[operator] || replaceUnderscoreWithSpace(operator);
|
||||
};
|
||||
|
||||
const formatFilterValue = value => {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'object' && value.name) {
|
||||
return value.name;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center w-full gap-2 mx-auto">
|
||||
<template
|
||||
v-for="(filter, index) in appliedFilters"
|
||||
:key="filter.attributeKey"
|
||||
>
|
||||
<div
|
||||
v-if="index < maxVisibleFilters"
|
||||
class="inline-flex items-center gap-2 h-7"
|
||||
>
|
||||
<div
|
||||
class="flex items-center h-full min-w-0 gap-1 px-2 py-1 text-xs border rounded-lg max-w-72 border-n-weak"
|
||||
>
|
||||
<span
|
||||
class="lowercase whitespace-nowrap first-letter:capitalize text-n-slate-12"
|
||||
>
|
||||
{{ replaceUnderscoreWithSpace(filter.attributeKey) }}
|
||||
</span>
|
||||
<span class="px-1 text-xs text-n-slate-10 whitespace-nowrap">
|
||||
{{ formatOperatorLabel(filter.filterOperator) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="filter.values"
|
||||
class="lowercase truncate text-n-slate-12"
|
||||
:class="{
|
||||
'first-letter:capitalize': shouldCapitalizeFirstLetter(
|
||||
filter.attributeKey
|
||||
),
|
||||
}"
|
||||
>
|
||||
{{ formatFilterValue(filter.values) }}
|
||||
</span>
|
||||
</div>
|
||||
<template
|
||||
v-if="
|
||||
index < maxVisibleFilters - 1 && index < appliedFilters.length - 1
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="content-center h-full px-1 text-xs font-medium uppercase rounded-lg text-n-slate-10"
|
||||
>
|
||||
{{ filter.queryOperator }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="appliedFilters.length > maxVisibleFilters"
|
||||
class="inline-flex items-center content-center px-1 text-xs rounded-lg text-n-slate-10 h-7"
|
||||
>
|
||||
{{ moreFiltersLabel }}
|
||||
</div>
|
||||
<div class="w-px h-3 rounded-lg bg-n-strong" />
|
||||
<Button
|
||||
:label="clearButtonLabel"
|
||||
size="xs"
|
||||
class="!px-1"
|
||||
variant="ghost"
|
||||
@click="emit('clearFilters')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -556,3 +556,48 @@ export const filterTypes = [
|
||||
attributeModel: 'customAttributes',
|
||||
},
|
||||
];
|
||||
|
||||
export const sampleActiveFilters = [
|
||||
{
|
||||
attributeKey: 'name',
|
||||
filterOperator: 'contains',
|
||||
values: 'John',
|
||||
queryOperator: 'and',
|
||||
},
|
||||
{
|
||||
attributeKey: 'email',
|
||||
filterOperator: 'does_not_contain',
|
||||
values: 'test@chatwoot.com',
|
||||
queryOperator: 'or',
|
||||
},
|
||||
{
|
||||
attributeKey: 'phone_number',
|
||||
filterOperator: 'is_present',
|
||||
values: '+928383822',
|
||||
queryOperator: 'and',
|
||||
},
|
||||
{
|
||||
attributeKey: 'created_at',
|
||||
filterOperator: 'is_greater_than',
|
||||
values: '2024-01-01',
|
||||
queryOperator: 'and',
|
||||
},
|
||||
{
|
||||
attributeKey: 'last_activity',
|
||||
filterOperator: 'days_before',
|
||||
values: '30',
|
||||
queryOperator: 'and',
|
||||
},
|
||||
{
|
||||
attributeKey: 'date_of_birth',
|
||||
filterOperator: 'is_not_present',
|
||||
values: '',
|
||||
queryOperator: 'and',
|
||||
},
|
||||
{
|
||||
attributeKey: 'country',
|
||||
filterOperator: 'not_equal_to',
|
||||
values: { id: 1, name: 'India' },
|
||||
queryOperator: 'and',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -39,3 +39,11 @@ export const buildAttributesFilterTypes = (attributes, getOperatorTypes) => {
|
||||
attributeModel: 'customAttributes',
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces underscores with spaces in a string
|
||||
* @param {string} text - The input string
|
||||
* @returns {string} The string with underscores replaced by spaces
|
||||
*/
|
||||
export const replaceUnderscoreWithSpace = text =>
|
||||
text?.replaceAll('_', ' ') ?? '';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
getCustomAttributeInputType,
|
||||
buildAttributesFilterTypes,
|
||||
replaceUnderscoreWithSpace,
|
||||
} from './filterHelper';
|
||||
|
||||
describe('filterHelper', () => {
|
||||
@@ -123,4 +124,14 @@ describe('filterHelper', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceUnderscoreWithSpace', () => {
|
||||
it('replaces underscores with spaces', () => {
|
||||
expect(replaceUnderscoreWithSpace('test_key')).toBe('test key');
|
||||
});
|
||||
|
||||
it('returns empty string if input is null', () => {
|
||||
expect(replaceUnderscoreWithSpace(null)).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,6 +75,7 @@ const pageInfo = computed(() => {
|
||||
icon="i-lucide-chevrons-left"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="slate"
|
||||
class="!w-8 !h-6"
|
||||
:disabled="isFirstPage"
|
||||
@click="changePage(1)"
|
||||
@@ -82,6 +83,7 @@ const pageInfo = computed(() => {
|
||||
<Button
|
||||
icon="i-lucide-chevron-left"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!w-8 !h-6"
|
||||
:disabled="isFirstPage"
|
||||
@@ -96,6 +98,7 @@ const pageInfo = computed(() => {
|
||||
<Button
|
||||
icon="i-lucide-chevron-right"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!w-8 !h-6"
|
||||
:disabled="isLastPage"
|
||||
@@ -104,6 +107,7 @@ const pageInfo = computed(() => {
|
||||
<Button
|
||||
icon="i-lucide-chevrons-right"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!w-8 !h-6"
|
||||
:disabled="isLastPage"
|
||||
|
||||
@@ -91,13 +91,18 @@ const activeCountry = computed(() =>
|
||||
const inputBorderClass = computed(() => {
|
||||
const errorClass =
|
||||
'border-n-ruby-8 dark:border-n-ruby-8 hover:border-n-ruby-9 dark:hover:border-n-ruby-9 disabled:border-n-ruby-8 dark:disabled:border-n-ruby-8';
|
||||
const focusClass =
|
||||
'has-[:focus]:border-n-brand dark:has-[:focus]:border-n-brand';
|
||||
|
||||
if (!props.showBorder) {
|
||||
return hasError.value ? errorClass : 'border-transparent';
|
||||
if (hasError.value) return errorClass;
|
||||
return `border-transparent ${focusClass}`;
|
||||
}
|
||||
|
||||
if (hasError.value) {
|
||||
return errorClass;
|
||||
}
|
||||
return 'has-[:focus]:border-n-brand dark:has-[:focus]:border-n-brand border-n-weak dark:border-n-weak hover:border-n-slate-6 dark:hover:border-n-slate-6 disabled:border-n-weak dark:disabled:border-n-weak';
|
||||
return `${focusClass} border-n-weak dark:border-n-weak hover:border-n-slate-6 dark:hover:border-n-slate-6 disabled:border-n-weak dark:disabled:border-n-weak`;
|
||||
});
|
||||
|
||||
const phoneNumberError = computed(() => {
|
||||
|
||||
@@ -401,7 +401,11 @@
|
||||
"ADD_CONTACT": "Add contact",
|
||||
"EXPORT_CONTACT": "Export contacts",
|
||||
"IMPORT_CONTACT": "Import contacts",
|
||||
"SAVE_CONTACT": "Save contact"
|
||||
"SAVE_CONTACT": "Save contact",
|
||||
"EMAIL_ADDRESS_DUPLICATE": "This email address is in use for another contact.",
|
||||
"PHONE_NUMBER_DUPLICATE": "This phone number is in use for another contact.",
|
||||
"SUCCESS_MESSAGE": "Contact saved successfully",
|
||||
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
|
||||
},
|
||||
"IMPORT_CONTACT": {
|
||||
"TITLE": "Import contacts",
|
||||
@@ -491,10 +495,9 @@
|
||||
"LABEL": "Segment name",
|
||||
"INPUT_PLACEHOLDER": "Enter the name of the segment"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard filters",
|
||||
"ADDITIONAL_FILTERS": "Additional filters",
|
||||
"CUSTOM_ATTRIBUTES": "Custom attributes"
|
||||
"ACTIVE_FILTERS": {
|
||||
"MORE_FILTERS": "+ {count} more filters",
|
||||
"CLEAR_FILTERS": "Clear filters"
|
||||
}
|
||||
},
|
||||
"CARD": {
|
||||
|
||||
@@ -118,6 +118,7 @@ const getCommonFetchParams = (page = 1) => ({
|
||||
});
|
||||
|
||||
const fetchContacts = async (page = 1) => {
|
||||
await store.dispatch('contacts/clearContactFilters');
|
||||
await store.dispatch('contacts/get', getCommonFetchParams(page));
|
||||
updatePageParam(page);
|
||||
};
|
||||
|
||||
@@ -70,6 +70,7 @@ export const actions = {
|
||||
data: response.data,
|
||||
filterType: FILTER_KEYS[obj.filter_type],
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
const errorMessage = error?.response?.data?.message;
|
||||
throw new Error(errorMessage);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# housekeeping
|
||||
# remove contact inboxes that does not have any conversations
|
||||
# and are older than 3 months
|
||||
|
||||
class Internal::ProcessStaleRedisKeysJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(account)
|
||||
Internal::RemoveStaleRedisKeysService.new(account_id: account.id).perform
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
# housekeeping
|
||||
# ensure stale ONLINE PRESENCE KEYS for contacts are removed periodically
|
||||
# should result in 50% redis mem size reduction
|
||||
|
||||
class Internal::RemoveStaleRedisKeysJob < ApplicationJob
|
||||
queue_as :scheduled_jobs
|
||||
|
||||
def perform
|
||||
Account.find_each do |account|
|
||||
Rails.logger.info "Enqueuing ProcessStaleRedisKeysJob for account #{account.id}"
|
||||
Internal::ProcessStaleRedisKeysJob.perform_later(account)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
class Internal::RemoveStaleRedisKeysService
|
||||
pattr_initialize [:account_id!]
|
||||
|
||||
def perform(account_id)
|
||||
range_start = (Time.zone.now - PRESENCE_DURATION).to_i
|
||||
# exclusive minimum score is specified by prefixing (
|
||||
# we are clearing old records because this could clogg up the sorted set
|
||||
::Redis::Alfred.zremrangebyscore(presence_key(account_id, 'Contact'), '-inf', "(#{range_start}")
|
||||
end
|
||||
end
|
||||
@@ -26,3 +26,10 @@ remove_stale_contact_inboxes_job.rb:
|
||||
cron: '30 22 * * *'
|
||||
class: 'Internal::RemoveStaleContactInboxesJob'
|
||||
queue: scheduled_jobs
|
||||
|
||||
# executed daily at 2230 UTC
|
||||
# which is our lowest traffic time
|
||||
remove_stale_redis_keys_job.rb:
|
||||
cron: '30 22 * * *'
|
||||
class: 'Internal::RemoveStaleRedisKeysJob'
|
||||
queue: scheduled_jobs
|
||||
|
||||
Generated
+20
-20
@@ -194,8 +194,8 @@ importers:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(vue@3.5.12(typescript@5.6.2))
|
||||
vue-i18n:
|
||||
specifier: 9.14.0
|
||||
version: 9.14.0(vue@3.5.12(typescript@5.6.2))
|
||||
specifier: 9.14.2
|
||||
version: 9.14.2(vue@3.5.12(typescript@5.6.2))
|
||||
vue-letter:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@@ -905,16 +905,16 @@ packages:
|
||||
'@internationalized/number@3.5.4':
|
||||
resolution: {integrity: sha512-h9huwWjNqYyE2FXZZewWqmCdkw1HeFds5q4Siuoms3hUQC5iPJK3aBmkFZoDSLN4UD0Bl8G22L/NdHpeOr+/7A==}
|
||||
|
||||
'@intlify/core-base@9.14.0':
|
||||
resolution: {integrity: sha512-zJn0imh9HIsZZUtt9v8T16PeVstPv6bP2YzlrYJwoF8F30gs4brZBwW2KK6EI5WYKFi3NeqX6+UU4gniz5TkGg==}
|
||||
'@intlify/core-base@9.14.2':
|
||||
resolution: {integrity: sha512-DZyQ4Hk22sC81MP4qiCDuU+LdaYW91A6lCjq8AWPvY3+mGMzhGDfOCzvyR6YBQxtlPjFqMoFk9ylnNYRAQwXtQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@9.14.0':
|
||||
resolution: {integrity: sha512-sXNsoMI0YsipSXW8SR75drmVK56tnJHoYbPXUv2Cf9lz6FzvwsosFm6JtC1oQZI/kU+n7qx0qRrEWkeYFTgETA==}
|
||||
'@intlify/message-compiler@9.14.2':
|
||||
resolution: {integrity: sha512-YsKKuV4Qv4wrLNsvgWbTf0E40uRv+Qiw1BeLQ0LAxifQuhiMe+hfTIzOMdWj/ZpnTDj4RSZtkXjJM7JDiiB5LQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@9.14.0':
|
||||
resolution: {integrity: sha512-r+N8KRQL7LgN1TMTs1A2svfuAU0J94Wu9wWdJVJqYsoMMLIeJxrPjazihfHpmJqfgZq0ah3Y9Q4pgWV2O90Fyg==}
|
||||
'@intlify/shared@9.14.2':
|
||||
resolution: {integrity: sha512-uRAHAxYPeF+G5DBIboKpPgC/Waecd4Jz8ihtkpJQD5ycb5PwXp0k/+hBGl5dAjwF7w+l74kz/PKA8r8OK//RUw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
@@ -4934,8 +4934,8 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '>=6.0.0'
|
||||
|
||||
vue-i18n@9.14.0:
|
||||
resolution: {integrity: sha512-LxmpRuCt2rI8gqU+kxeflRZMQn4D5+4M3oP3PWZdowW/ePJraHqhF7p4CuaME52mUxdw3Mmy2yAUKgfZYgCRjA==}
|
||||
vue-i18n@9.14.2:
|
||||
resolution: {integrity: sha512-JK9Pm80OqssGJU2Y6F7DcM8RFHqVG4WkuCqOZTVsXkEzZME7ABejAUqUdA931zEBedc4thBgSUWxeQh4uocJAQ==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
@@ -5770,17 +5770,17 @@ snapshots:
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.15
|
||||
|
||||
'@intlify/core-base@9.14.0':
|
||||
'@intlify/core-base@9.14.2':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 9.14.0
|
||||
'@intlify/shared': 9.14.0
|
||||
'@intlify/message-compiler': 9.14.2
|
||||
'@intlify/shared': 9.14.2
|
||||
|
||||
'@intlify/message-compiler@9.14.0':
|
||||
'@intlify/message-compiler@9.14.2':
|
||||
dependencies:
|
||||
'@intlify/shared': 9.14.0
|
||||
'@intlify/shared': 9.14.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@9.14.0': {}
|
||||
'@intlify/shared@9.14.2': {}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
@@ -10406,11 +10406,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vue-i18n@9.14.0(vue@3.5.12(typescript@5.6.2)):
|
||||
vue-i18n@9.14.2(vue@3.5.12(typescript@5.6.2)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 9.14.0
|
||||
'@intlify/shared': 9.14.0
|
||||
'@vue/devtools-api': 6.6.3
|
||||
'@intlify/core-base': 9.14.2
|
||||
'@intlify/shared': 9.14.2
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.12(typescript@5.6.2)
|
||||
|
||||
vue-letter@0.2.0:
|
||||
|
||||
Reference in New Issue
Block a user