Skip to content

Commit 9cdf2ad

Browse files
Merge branch 'chatwoot:develop' into fazer-ai/main
2 parents d7358c8 + 94baba1 commit 9cdf2ad

File tree

13 files changed

+283
-200
lines changed

13 files changed

+283
-200
lines changed

app/javascript/dashboard/components-next/Contacts/ContactsDetailsLayout.vue

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
88
import ComposeConversation from 'dashboard/components-next/NewConversation/ComposeConversation.vue';
99
1010
const props = defineProps({
11-
buttonLabel: {
12-
type: String,
13-
default: '',
14-
},
1511
selectedContact: {
1612
type: Object,
1713
default: () => ({}),
1814
},
15+
isUpdating: {
16+
type: Boolean,
17+
default: false,
18+
},
1919
});
2020
21-
const emit = defineEmits(['goToContactsList']);
21+
const emit = defineEmits(['goToContactsList', 'toggleBlock']);
2222
2323
const { t } = useI18n();
2424
const slots = useSlots();
@@ -45,9 +45,17 @@ const breadcrumbItems = computed(() => {
4545
return items;
4646
});
4747
48+
const isContactBlocked = computed(() => {
49+
return props.selectedContact?.blocked;
50+
});
51+
4852
const handleBreadcrumbClick = () => {
4953
emit('goToContactsList');
5054
};
55+
56+
const toggleBlock = () => {
57+
emit('toggleBlock', isContactBlocked.value);
58+
};
5159
</script>
5260
5361
<template>
@@ -64,11 +72,29 @@ const handleBreadcrumbClick = () => {
6472
:items="breadcrumbItems"
6573
@click="handleBreadcrumbClick"
6674
/>
67-
<ComposeConversation :contact-id="contactId">
68-
<template #trigger="{ toggle }">
69-
<Button :label="buttonLabel" size="sm" @click="toggle" />
70-
</template>
71-
</ComposeConversation>
75+
<div class="flex items-center gap-2">
76+
<Button
77+
:label="
78+
!isContactBlocked
79+
? $t('CONTACTS_LAYOUT.HEADER.BLOCK_CONTACT')
80+
: $t('CONTACTS_LAYOUT.HEADER.UNBLOCK_CONTACT')
81+
"
82+
size="sm"
83+
slate
84+
:is-loading="isUpdating"
85+
:disabled="isUpdating"
86+
@click="toggleBlock"
87+
/>
88+
<ComposeConversation :contact-id="contactId">
89+
<template #trigger="{ toggle }">
90+
<Button
91+
:label="$t('CONTACTS_LAYOUT.HEADER.SEND_MESSAGE')"
92+
size="sm"
93+
@click="toggle"
94+
/>
95+
</template>
96+
</ComposeConversation>
97+
</div>
7298
</div>
7399
</div>
74100
</header>

app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,15 @@ const isOutgoing = computed(() => {
2929
});
3030
const isIncoming = computed(() => !isOutgoing.value);
3131
32+
const textToShow = computed(() => {
33+
const text =
34+
contentAttributes?.value?.email?.textContent?.full ?? content.value;
35+
return text?.replace(/\n/g, '<br>');
36+
});
37+
38+
// Use TextContent as the default to fullHTML
3239
const fullHTML = computed(() => {
33-
return contentAttributes?.value?.email?.htmlContent?.full ?? content.value;
40+
return contentAttributes?.value?.email?.htmlContent?.full ?? textToShow.value;
3441
});
3542
3643
const unquotedHTML = computed(() => {
@@ -40,12 +47,6 @@ const unquotedHTML = computed(() => {
4047
const hasQuotedMessage = computed(() => {
4148
return EmailQuoteExtractor.hasQuotes(fullHTML.value);
4249
});
43-
44-
const textToShow = computed(() => {
45-
const text =
46-
contentAttributes?.value?.email?.textContent?.full ?? content.value;
47-
return text?.replace(/\n/g, '<br>');
48-
});
4950
</script>
5051
5152
<template>

app/javascript/dashboard/i18n/locale/en/contact.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@
289289
"SEARCH_PLACEHOLDER": "Search...",
290290
"MESSAGE_BUTTON": "Message",
291291
"SEND_MESSAGE": "Send message",
292+
"BLOCK_CONTACT": "Block contact",
293+
"UNBLOCK_CONTACT": "Unblock contact",
292294
"BREADCRUMB": {
293295
"CONTACTS": "Contacts"
294296
},
@@ -303,6 +305,10 @@
303305
"SUCCESS_MESSAGE": "Contact saved successfully",
304306
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
305307
},
308+
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
309+
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
310+
"UNBLOCK_SUCCESS_MESSAGE": "This contact is unblocked successfully",
311+
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
306312
"IMPORT_CONTACT": {
307313
"TITLE": "Import contacts",
308314
"DESCRIPTION": "Import contacts through a CSV file.",

app/javascript/dashboard/routes/dashboard/contacts/pages/ContactManageView.vue

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup>
22
import { onMounted, computed, ref } from 'vue';
33
import { useI18n } from 'vue-i18n';
4+
import { useAlert } from 'dashboard/composables';
45
import { useStore, useMapGetter } from 'dashboard/composables/store';
56
import { useRoute, useRouter } from 'vue-router';
67
@@ -25,6 +26,7 @@ const contactMergeRef = ref(null);
2526
2627
const isFetchingItem = computed(() => uiFlags.value.isFetchingItem);
2728
const isMergingContact = computed(() => uiFlags.value.isMerging);
29+
const isUpdatingContact = computed(() => uiFlags.value.isUpdating);
2830
2931
const selectedContact = computed(() => contact.value(route.params.contactId));
3032
@@ -88,6 +90,33 @@ const fetchAttributes = () => {
8890
store.dispatch('attributes/get');
8991
};
9092
93+
const toggleContactBlock = async isBlocked => {
94+
const ALERT_MESSAGES = {
95+
success: {
96+
block: t('CONTACTS_LAYOUT.HEADER.ACTIONS.BLOCK_SUCCESS_MESSAGE'),
97+
unblock: t('CONTACTS_LAYOUT.HEADER.ACTIONS.UNBLOCK_SUCCESS_MESSAGE'),
98+
},
99+
error: {
100+
block: t('CONTACTS_LAYOUT.HEADER.ACTIONS.BLOCK_ERROR_MESSAGE'),
101+
unblock: t('CONTACTS_LAYOUT.HEADER.ACTIONS.UNBLOCK_ERROR_MESSAGE'),
102+
},
103+
};
104+
105+
try {
106+
await store.dispatch(`contacts/update`, {
107+
...selectedContact.value,
108+
blocked: !isBlocked,
109+
});
110+
useAlert(
111+
isBlocked ? ALERT_MESSAGES.success.unblock : ALERT_MESSAGES.success.block
112+
);
113+
} catch (error) {
114+
useAlert(
115+
isBlocked ? ALERT_MESSAGES.error.unblock : ALERT_MESSAGES.error.block
116+
);
117+
}
118+
};
119+
91120
onMounted(() => {
92121
fetchActiveContact();
93122
fetchContactNotes();
@@ -105,7 +134,9 @@ onMounted(() => {
105134
:selected-contact="selectedContact"
106135
is-detail-view
107136
:show-pagination-footer="false"
137+
:is-updating="isUpdatingContact"
108138
@go-to-contacts-list="goToContactsList"
139+
@toggle-block="toggleContactBlock"
109140
>
110141
<div
111142
v-if="showSpinner"
Lines changed: 85 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,113 @@
1-
<script>
2-
import { mapGetters } from 'vuex';
1+
<script setup>
2+
import { computed, onMounted, ref } from 'vue';
3+
import { useStore, useMapGetter } from 'dashboard/composables/store';
34
import { useAccount } from 'dashboard/composables/useAccount';
5+
import { useUISettings } from 'dashboard/composables/useUISettings';
6+
7+
import Draggable from 'vuedraggable';
8+
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
49
import MacroItem from './MacroItem.vue';
510
6-
export default {
7-
components: {
8-
MacroItem,
9-
},
10-
props: {
11-
conversationId: {
12-
type: [Number, String],
13-
required: true,
14-
},
11+
defineProps({
12+
conversationId: {
13+
type: [Number, String],
14+
required: true,
1515
},
16-
setup() {
17-
const { accountScopedUrl } = useAccount();
16+
});
1817
19-
return {
20-
accountScopedUrl,
21-
};
22-
},
23-
computed: {
24-
...mapGetters({
25-
macros: ['macros/getMacros'],
26-
uiFlags: 'macros/getUIFlags',
27-
}),
18+
const store = useStore();
19+
const { accountScopedUrl } = useAccount();
20+
const { uiSettings, updateUISettings } = useUISettings();
21+
22+
const dragging = ref(false);
23+
24+
const macros = useMapGetter('macros/getMacros');
25+
const uiFlags = useMapGetter('macros/getUIFlags');
26+
27+
const MACROS_ORDER_KEY = 'macros_display_order';
28+
29+
const orderedMacros = computed({
30+
get: () => {
31+
// Get saved order array and current macros
32+
const savedOrder = uiSettings.value?.[MACROS_ORDER_KEY] ?? [];
33+
const currentMacros = macros.value ?? [];
34+
35+
// Return unmodified macros if not present or macro is not available
36+
if (!savedOrder.length || !currentMacros.length) {
37+
return currentMacros;
38+
}
39+
40+
// Create a Map of id -> position for faster lookups
41+
const orderMap = new Map(savedOrder.map((id, index) => [id, index]));
42+
43+
return [...currentMacros].sort((a, b) => {
44+
// Use Infinity for items not in saved order (pushes them to end)
45+
const aPos = orderMap.get(a.id) ?? Infinity;
46+
const bPos = orderMap.get(b.id) ?? Infinity;
47+
return aPos - bPos;
48+
});
2849
},
29-
mounted() {
30-
this.$store.dispatch('macros/get');
50+
set: newOrder => {
51+
// Update settings with array of ids from new order
52+
updateUISettings({
53+
[MACROS_ORDER_KEY]: newOrder.map(({ id }) => id),
54+
});
3155
},
56+
});
57+
58+
const onDragEnd = () => {
59+
dragging.value = false;
3260
};
61+
62+
onMounted(() => {
63+
store.dispatch('macros/get');
64+
});
3365
</script>
3466
3567
<template>
3668
<div>
37-
<div
38-
v-if="!uiFlags.isFetching && !macros.length"
39-
class="macros_list--empty-state"
40-
>
69+
<div v-if="!uiFlags.isFetching && !macros.length" class="p-3">
4170
<p class="flex flex-col items-center justify-center h-full">
4271
{{ $t('MACROS.LIST.404') }}
4372
</p>
4473
<router-link :to="accountScopedUrl('settings/macros')">
45-
<woot-button
46-
variant="smooth"
47-
icon="add"
48-
size="tiny"
49-
class="macros_add-button"
50-
>
74+
<woot-button variant="smooth" icon="add" size="tiny" class="mt-1">
5175
{{ $t('MACROS.HEADER_BTN_TXT') }}
5276
</woot-button>
5377
</router-link>
5478
</div>
55-
<woot-loading-state
79+
<div
5680
v-if="uiFlags.isFetching"
57-
:message="$t('MACROS.LOADING')"
58-
/>
59-
<div v-if="!uiFlags.isFetching && macros.length" class="macros-list">
60-
<MacroItem
61-
v-for="macro in macros"
62-
:key="macro.id"
63-
:macro="macro"
64-
:conversation-id="conversationId"
65-
/>
81+
class="flex items-center gap-2 justify-center p-6 text-n-slate-12"
82+
>
83+
<span class="text-sm">{{ $t('MACROS.LOADING') }}</span>
84+
<Spinner class="size-5" />
6685
</div>
86+
<Draggable
87+
v-if="!uiFlags.isFetching && macros.length"
88+
v-model="orderedMacros"
89+
class="p-1"
90+
animation="200"
91+
ghost-class="ghost"
92+
handle=".drag-handle"
93+
item-key="id"
94+
@start="dragging = true"
95+
@end="onDragEnd"
96+
>
97+
<template #item="{ element }">
98+
<MacroItem
99+
:key="element.id"
100+
:macro="element"
101+
:conversation-id="conversationId"
102+
class="drag-handle cursor-grab"
103+
/>
104+
</template>
105+
</Draggable>
67106
</div>
68107
</template>
69108
70109
<style scoped lang="scss">
71-
.macros-list {
72-
padding: var(--space-smaller);
73-
}
74-
.macros_list--empty-state {
75-
padding: var(--space-slab);
76-
p {
77-
margin: 0;
78-
}
79-
}
80-
.macros_add-button {
81-
margin: var(--space-small) auto 0;
110+
.ghost {
111+
@apply opacity-50 bg-n-slate-3 dark:bg-n-slate-9;
82112
}
83113
</style>

0 commit comments

Comments
 (0)