Skip to content

Commit fa2698c

Browse files
feat: add per-inbox signature management
- Introduced `InboxSignature` model to manage signatures specific to each inbox. - Added API endpoints for fetching, creating, updating, and deleting inbox signatures. - Updated UI components to support inbox-specific signatures, including overrides for signature position and separator. - Implemented a new composable `useInboxSignatures` for managing inbox signatures in the frontend. - Enhanced existing components to utilize inbox signatures, including the reply box and message signature settings. - Added tests for the new inbox signatures functionality, ensuring proper behavior of the API and model validations. - Updated translations for new UI elements related to inbox signatures.
1 parent 21007bd commit fa2698c

File tree

29 files changed

+903
-36
lines changed

29 files changed

+903
-36
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
class Api::V1::Profile::InboxSignaturesController < Api::BaseController
2+
before_action :set_user
3+
before_action :set_inbox_signature, only: %i[show update destroy]
4+
5+
def index
6+
@inbox_signatures = if params[:account_id].present?
7+
account_inbox_ids = Account.find(params[:account_id]).inbox_ids
8+
@user.inbox_signatures.where(inbox_id: account_inbox_ids)
9+
else
10+
@user.inbox_signatures
11+
end
12+
end
13+
14+
def show
15+
head :not_found and return unless @inbox_signature
16+
end
17+
18+
def update
19+
if @inbox_signature
20+
@inbox_signature.update!(inbox_signature_params)
21+
else
22+
@inbox_signature = @user.inbox_signatures.create!(
23+
inbox_signature_params.merge(inbox_id: params[:inbox_id])
24+
)
25+
end
26+
end
27+
28+
def destroy
29+
@inbox_signature&.destroy!
30+
head :no_content
31+
end
32+
33+
private
34+
35+
def set_user
36+
@user = current_user
37+
end
38+
39+
def set_inbox_signature
40+
@inbox_signature = @user.inbox_signatures.find_by(inbox_id: params[:inbox_id])
41+
end
42+
43+
def inbox_signature_params
44+
params.require(:inbox_signature).permit(:message_signature, :signature_position, :signature_separator)
45+
end
46+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* global axios */
2+
3+
const API_BASE = '/api/v1/profile/inbox_signatures';
4+
5+
export default {
6+
getAll(accountId) {
7+
return axios.get(API_BASE, {
8+
params: { account_id: accountId },
9+
});
10+
},
11+
12+
get(inboxId) {
13+
return axios.get(`${API_BASE}/${inboxId}`);
14+
},
15+
16+
upsert(inboxId, params) {
17+
return axios.put(`${API_BASE}/${inboxId}`, {
18+
inbox_signature: params,
19+
});
20+
},
21+
22+
delete(inboxId) {
23+
return axios.delete(`${API_BASE}/${inboxId}`);
24+
},
25+
};

app/javascript/dashboard/components-next/NewConversation/ComposeConversation.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useStore, useMapGetter } from 'dashboard/composables/store';
44
import { useI18n } from 'vue-i18n';
55
import { useWindowSize } from '@vueuse/core';
66
import { useUISettings } from 'dashboard/composables/useUISettings';
7+
import { useInboxSignatures } from 'dashboard/composables/useInboxSignatures';
78
import { vOnClickOutside } from '@vueuse/components';
89
import { useAlert } from 'dashboard/composables';
910
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
@@ -84,6 +85,24 @@ const uiFlags = useMapGetter('contactConversations/getUIFlags');
8485
const messageSignature = useMapGetter('getMessageSignature');
8586
const inboxesList = useMapGetter('inboxes/getInboxes');
8687
88+
const {
89+
fetchInboxSignatures,
90+
getSignatureForInbox,
91+
getSignatureSettingsForInbox,
92+
} = useInboxSignatures();
93+
94+
fetchInboxSignatures();
95+
96+
const resolvedMessageSignature = computed(() => {
97+
if (!targetInbox.value?.id) return messageSignature.value;
98+
return getSignatureForInbox(targetInbox.value.id);
99+
});
100+
101+
const resolvedSignatureSettings = computed(() => {
102+
if (!targetInbox.value?.id) return null;
103+
return getSignatureSettingsForInbox(targetInbox.value.id);
104+
});
105+
87106
const sendWithSignature = computed(() =>
88107
fetchSignatureFlagFromUISettings(targetInbox.value?.channelType)
89108
);
@@ -307,8 +326,9 @@ useKeyboardEvents(keyboardEvents);
307326
:is-direct-uploads-enabled="directUploadsEnabled"
308327
:contact-conversations-ui-flags="uiFlags"
309328
:contacts-ui-flags="contactsUiFlags"
310-
:message-signature="messageSignature"
329+
:message-signature="resolvedMessageSignature"
311330
:send-with-signature="sendWithSignature"
331+
:signature-settings="resolvedSignatureSettings"
312332
@search-contacts="onContactSearch"
313333
@reset-contact-search="resetContacts"
314334
@update-selected-contact="handleSelectedContact"

app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const props = defineProps({
3535
contactsUiFlags: { type: Object, default: null },
3636
messageSignature: { type: String, default: '' },
3737
sendWithSignature: { type: Boolean, default: false },
38+
signatureSettings: { type: Object, default: null },
3839
formState: { type: Object, required: true },
3940
});
4041
@@ -131,6 +132,7 @@ const newMessagePayload = () => {
131132
directUploadsEnabled: props.isDirectUploadsEnabled,
132133
sendWithSignature: props.sendWithSignature,
133134
messageSignature: props.messageSignature,
135+
signatureSettings: props.signatureSettings,
134136
});
135137
};
136138

app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,20 +132,15 @@ export const prepareNewMessagePayload = ({
132132
directUploadsEnabled = false,
133133
sendWithSignature = false,
134134
messageSignature = '',
135+
signatureSettings = null,
135136
}) => {
136137
let finalMessage = message;
137138
if (sendWithSignature && messageSignature) {
138-
const { signature_position, signature_separator } =
139-
currentUser?.ui_settings || {};
140-
const signatureSettings = {
141-
position: signature_position || 'top',
142-
separator: signature_separator || 'blank',
139+
const settings = signatureSettings || {
140+
position: currentUser?.ui_settings?.signature_position || 'top',
141+
separator: currentUser?.ui_settings?.signature_separator || 'blank',
143142
};
144-
finalMessage = appendSignature(
145-
message,
146-
messageSignature,
147-
signatureSettings
148-
);
143+
finalMessage = appendSignature(message, messageSignature, settings);
149144
}
150145

151146
const payload = {

app/javascript/dashboard/components/widgets/WootWriter/Editor.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ const props = defineProps({
8787
// allowSignature is a kill switch, ensuring no signature methods
8888
// are triggered except when this flag is true
8989
allowSignature: { type: Boolean, default: false },
90+
// Per-inbox overrides; when empty, falls back to currentUser.ui_settings
91+
signaturePositionOverride: { type: String, default: '' },
92+
signatureSeparatorOverride: { type: String, default: '' },
9093
channelType: { type: String, default: '' },
9194
conversationId: { type: Number, default: null },
9295
medium: { type: String, default: '' },
@@ -322,11 +325,19 @@ const sendWithSignature = computed(() => {
322325
});
323326
324327
const signaturePosition = computed(() => {
325-
return currentUser.value?.ui_settings?.signature_position || 'top';
328+
return (
329+
props.signaturePositionOverride ||
330+
currentUser.value?.ui_settings?.signature_position ||
331+
'top'
332+
);
326333
});
327334
328335
const signatureSeparator = computed(() => {
329-
return currentUser.value?.ui_settings?.signature_separator || 'blank';
336+
return (
337+
props.signatureSeparatorOverride ||
338+
currentUser.value?.ui_settings?.signature_separator ||
339+
'blank'
340+
);
330341
});
331342
332343
const shouldShowSignaturePreview = computed(() => {
@@ -850,6 +861,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
850861
<!-- Signature preview at top -->
851862
<div
852863
v-if="shouldShowSignaturePreview && signaturePosition === 'top'"
864+
v-tooltip="t('CONVERSATION.FOOTER.SIGNATURE_LABEL_TOP_TOOLTIP')"
853865
class="signature-preview signature-preview--top"
854866
>
855867
<div class="signature-label">
@@ -864,6 +876,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
864876
<!-- Signature preview at bottom -->
865877
<div
866878
v-if="shouldShowSignaturePreview && signaturePosition === 'bottom'"
879+
v-tooltip="t('CONVERSATION.FOOTER.SIGNATURE_LABEL_BOTTOM_TOOLTIP')"
867880
class="signature-preview signature-preview--bottom"
868881
>
869882
<div class="signature-label">
@@ -899,7 +912,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
899912
@import '@chatwoot/prosemirror-schema/src/styles/base.scss';
900913
901914
.signature-preview {
902-
@apply px-1 py-1 text-n-slate-10 text-sm pointer-events-none select-none opacity-70;
915+
@apply px-1 py-1 text-n-slate-10 text-sm select-none opacity-70 cursor-default;
903916
904917
&--top {
905918
@apply border-b border-n-weak pb-1;

app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { defineAsyncComponent, useTemplateRef } from 'vue';
33
import { mapGetters } from 'vuex';
44
import { useAlert } from 'dashboard/composables';
55
import { useUISettings } from 'dashboard/composables/useUISettings';
6+
import { useInboxSignatures } from 'dashboard/composables/useInboxSignatures';
67
import { useTrack } from 'dashboard/composables';
78
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
89
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
@@ -97,6 +98,15 @@ export default {
9798
fetchQuotedReplyFlagFromUISettings,
9899
} = useUISettings();
99100
101+
const {
102+
fetchInboxSignatures,
103+
getSignatureForInbox,
104+
getSignatureSettingsForInbox,
105+
hasFetched: inboxSignaturesFetched,
106+
} = useInboxSignatures();
107+
108+
fetchInboxSignatures();
109+
100110
const { formatMessage } = useMessageFormatter();
101111
102112
const replyEditor = useTemplateRef('replyEditor');
@@ -109,6 +119,9 @@ export default {
109119
fetchSignatureFlagFromUISettings,
110120
setQuotedReplyFlagForInbox,
111121
fetchQuotedReplyFlagFromUISettings,
122+
getSignatureForInbox,
123+
getSignatureSettingsForInbox,
124+
inboxSignaturesFetched,
112125
replyEditor,
113126
copilot,
114127
shortcutKey,
@@ -147,7 +160,6 @@ export default {
147160
computed: {
148161
...mapGetters({
149162
currentChat: 'getSelectedChat',
150-
messageSignature: 'getMessageSignature',
151163
currentUser: 'getCurrentUser',
152164
lastEmail: 'getLastEmailInSelectedChat',
153165
globalConfig: 'globalConfig/get',
@@ -344,6 +356,9 @@ export default {
344356
isSignatureEnabledForInbox() {
345357
return !this.isPrivate && this.sendWithSignature;
346358
},
359+
messageSignature() {
360+
return this.getSignatureForInbox(this.inboxId);
361+
},
347362
isSignatureAvailable() {
348363
return !!this.messageSignature;
349364
},
@@ -449,10 +464,10 @@ export default {
449464
);
450465
},
451466
signaturePosition() {
452-
return this.currentUser?.ui_settings?.signature_position || 'top';
467+
return this.getSignatureSettingsForInbox(this.inboxId).position;
453468
},
454469
signatureSeparator() {
455-
return this.currentUser?.ui_settings?.signature_separator || 'blank';
470+
return this.getSignatureSettingsForInbox(this.inboxId).separator;
456471
},
457472
formattedSignature() {
458473
if (!this.messageSignature) return '';
@@ -703,11 +718,9 @@ export default {
703718
if (!this.sendWithSignature || !this.messageSignature) {
704719
return message;
705720
}
706-
const { signature_position, signature_separator } =
707-
this.currentUser?.ui_settings || {};
708721
const signatureSettings = {
709-
position: signature_position || 'top',
710-
separator: signature_separator || 'blank',
722+
position: this.signaturePosition,
723+
separator: this.signatureSeparator,
711724
};
712725
return appendSignature(message, this.messageSignature, signatureSettings);
713726
},
@@ -1354,6 +1367,8 @@ export default {
13541367
:variables="messageVariables"
13551368
:signature="messageSignature"
13561369
allow-signature
1370+
:signature-position-override="signaturePosition"
1371+
:signature-separator-override="signatureSeparator"
13571372
:channel-type="channelType"
13581373
:medium="inbox.medium"
13591374
@typing-off="onTypingOff"

0 commit comments

Comments
 (0)