feat: add customizable signature position and separator options (#78)
* feat: add customizable signature position and separator options * fix: correct default value note for signatureSeparator and ensure reactivity * fix: correct watcher boolean conversion and add immediate ui_settings updates - Fix watchers to convert string props to boolean values for reactive refs - Add immediate event handlers for switch changes to update ui_settings in real-time - Ensure proper synchronization between switch states and user.ui_settings Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: split signature content and ui_settings updates to resolve persistence bug - Use updateUISettings store action for signature_position and signature_separator - Keep updateProfile for message_signature content only - Fixes FormData serialization issue that corrupted nested ui_settings object - Add diagnostic logging to verify data flow Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * clean: remove diagnostic console logging from updateSignature method - Remove temporary console.log statements added for verification - Keep core implementation that splits signature content and ui_settings updates - Keep console.error for proper error handling with eslint-disable comment - Implementation now ready for production use Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: updateUISettings call in updateSignature method * chore: move signature application to send-time and add button highlighting (#79) * fix: move signature application from editor manipulation to send-time - Remove addSignature/removeSignature/toggleSignatureInEditor from WootWriter - Remove signature logic from draft handling and canned response insertion - Apply signatures only in getMessagePayload during message sending - Add button highlighting for signature toggle when activated - Prevents signature duplication and persistence in editor content - Fixes signature position toggle bug Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: escape signature separator to prevent markdown setext heading interpretation - Escape '--' separator as '\--' in appendSignature to prevent H2 heading creation - Update removeSignature to handle escaped separators correctly - Fixes signature separator being rendered as markdown instead of plain text - Refactor nested ternary to fix ESLint error Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: prevent signature separator markdown interpretation in message processing - Add fix_signature_separator_markdown method to escape '--' separators - Update ensure_processed_message_content to fix separators before saving - Prevents signature separators from being interpreted as setext headings - Ensures correct message display in channels and email notifications Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: update separator format to use \n--\n instead of escaping - Change separator delimiter from '\--' to '\n--\n' format - Update removeSignature function to handle new separator format correctly - Simplify message processing since separators are already properly formatted - Ensures consistent separator handling across frontend and backend Co-Authored-By: cayo@fazer.ai <cayoproliveira@gmail.com> * fix: update signature delimiter format to include extra new lines * chore: remove comment about signature application logic * refactor: remove unused method and comments related to signature separator markdown processing * chore: simplify slash command detection by using updatedMessage directly * refactor: remove signature logic from draft message handling * refactor: simplify body empty check by removing signature manipulation logic --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cayo@fazer.ai <cayoproliveira@gmail.com> * refactor: extract signature settings logic into a separate method * fix: handle nil ui_settings in signature position and separator methods * fix: update return value of findSignatureInBody to include position information * fix: update signature handling in findSignatureInBody and related methods * fix: adjust delimiter length handling in removeSignature function * test: add cases for appending, removing, and replacing signatures with various separators * test: add cases for signature position and separator handling * test: add cases for updating signature position and separator in ui_settings * fix: correct typo in comment for findSignatureInBody function * refactor: simplify translation function calls in MessageSignature component * chore: refactoring * chore: refactor * feat: switch -> select * chore: refactor and undo changes * chore: refactor and undo changes * chore: refactor * fix: remove old select component usage * chore: remove useless style --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
This commit is contained in:
parent
587623a09f
commit
c6f9e814c2
@ -22,6 +22,7 @@ import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { CONVERSATION_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
@ -45,11 +46,10 @@ import {
|
||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
||||
|
||||
import {
|
||||
appendSignature,
|
||||
findNodeToInsertImage,
|
||||
getContentNode,
|
||||
cleanSignature,
|
||||
insertAtCursor,
|
||||
removeSignature as removeSignatureHelper,
|
||||
scrollCursorIntoView,
|
||||
setURLWithQueryAndSize,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
@ -123,6 +123,8 @@ const createState = (
|
||||
const { isEditorHotKeyEnabled, fetchSignatureFlagFromUISettings } =
|
||||
useUISettings();
|
||||
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
|
||||
const typingIndicator = createTypingIndicator(
|
||||
() => emit('typingOn'),
|
||||
() => emit('typingOff'),
|
||||
@ -266,8 +268,27 @@ watch(showVariables, updatedValue => {
|
||||
function focusEditorInputField(pos = 'end') {
|
||||
const { tr } = editorView.state;
|
||||
|
||||
const selection =
|
||||
pos === 'end' ? Selection.atEnd(tr.doc) : Selection.atStart(tr.doc);
|
||||
// Check if signature is at start and adjust cursor position accordingly
|
||||
const signaturePosition =
|
||||
currentUser.value?.ui_settings?.signature_position || 'top';
|
||||
const hasSignature = sendWithSignature.value && props.signature;
|
||||
|
||||
let selection;
|
||||
if (pos === 'end' || !hasSignature || signaturePosition !== 'top') {
|
||||
selection =
|
||||
pos === 'end' ? Selection.atEnd(tr.doc) : Selection.atStart(tr.doc);
|
||||
} else {
|
||||
// Position cursor after signature when signature is at start
|
||||
const signatureLength = props.signature
|
||||
? cleanSignature(props.signature).length
|
||||
: 0;
|
||||
const separatorLength =
|
||||
currentUser.value?.ui_settings?.signature_separator === '--' ? 6 : 2; // "\n--\n" vs "\n\n"
|
||||
const cursorPos = signatureLength + separatorLength;
|
||||
selection = Selection.near(
|
||||
tr.doc.resolve(Math.min(cursorPos, tr.doc.content.size))
|
||||
);
|
||||
}
|
||||
|
||||
editorView.dispatch(tr.setSelection(selection));
|
||||
editorView.focus();
|
||||
@ -277,14 +298,8 @@ function isBodyEmpty(content) {
|
||||
// if content is undefined, we assume that the body is empty
|
||||
if (!content) return true;
|
||||
|
||||
// if the signature is present, we need to remove it before checking
|
||||
// note that we don't update the editorView, so this is safe
|
||||
const bodyWithoutSignature = props.signature
|
||||
? removeSignatureHelper(content, props.signature)
|
||||
: content;
|
||||
|
||||
// trimming should remove all the whitespaces, so we can check the length
|
||||
return bodyWithoutSignature.trim().length === 0;
|
||||
return content.trim().length === 0;
|
||||
}
|
||||
|
||||
function handleEmptyBodyWithSignature() {
|
||||
@ -334,39 +349,6 @@ function reloadState(content = props.modelValue) {
|
||||
focusEditor(unrefContent);
|
||||
}
|
||||
|
||||
function addSignature() {
|
||||
let content = props.modelValue;
|
||||
// see if the content is empty, if it is before appending the signature
|
||||
// we need to add a paragraph node and move the cursor at the start of the editor
|
||||
const contentWasEmpty = isBodyEmpty(content);
|
||||
content = appendSignature(content, props.signature);
|
||||
// need to reload first, ensuring that the editorView is updated
|
||||
reloadState(content);
|
||||
|
||||
if (contentWasEmpty) {
|
||||
handleEmptyBodyWithSignature();
|
||||
}
|
||||
}
|
||||
|
||||
function removeSignature() {
|
||||
if (!props.signature) return;
|
||||
let content = props.modelValue;
|
||||
content = removeSignatureHelper(content, props.signature);
|
||||
// reload the state, ensuring that the editorView is updated
|
||||
reloadState(content);
|
||||
}
|
||||
|
||||
function toggleSignatureInEditor(signatureEnabled) {
|
||||
// The toggleSignatureInEditor gets the new value from the
|
||||
// watcher, this means that if the value is true, the signature
|
||||
// is supposed to be added, else we remove it.
|
||||
if (signatureEnabled) {
|
||||
addSignature();
|
||||
} else {
|
||||
removeSignature();
|
||||
}
|
||||
}
|
||||
|
||||
function setToolbarPosition() {
|
||||
const editorRect = editorRoot.value.getBoundingClientRect();
|
||||
const rect = selectedImageNode.value.getBoundingClientRect();
|
||||
@ -650,13 +632,6 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
watch(sendWithSignature, newValue => {
|
||||
// see if the allowSignature flag is true
|
||||
if (props.allowSignature) {
|
||||
toggleSignatureInEditor(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// [VITE] state assignment was done in created before
|
||||
state = createState(
|
||||
|
||||
@ -327,7 +327,7 @@ export default {
|
||||
v-if="showMessageSignatureButton"
|
||||
v-tooltip.top-end="signatureToggleTooltip"
|
||||
icon="i-ph-signature"
|
||||
slate
|
||||
:color="sendWithSignature ? 'blue' : 'slate'"
|
||||
faded
|
||||
sm
|
||||
@click="toggleMessageSignature"
|
||||
|
||||
@ -35,8 +35,6 @@ import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||
import {
|
||||
appendSignature,
|
||||
removeSignature,
|
||||
replaceSignature,
|
||||
extractTextFromMarkdown,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
|
||||
@ -433,11 +431,7 @@ export default {
|
||||
},
|
||||
message(updatedMessage) {
|
||||
// Check if the message starts with a slash.
|
||||
const bodyWithoutSignature = removeSignature(
|
||||
updatedMessage,
|
||||
this.signatureToApply
|
||||
);
|
||||
const startsWithSlash = bodyWithoutSignature.startsWith('/');
|
||||
const startsWithSlash = updatedMessage.startsWith('/');
|
||||
|
||||
// Determine if the user is potentially typing a slash command.
|
||||
// This is true if the message starts with a slash and the rich content editor is not active.
|
||||
@ -447,7 +441,7 @@ export default {
|
||||
// If a slash command is active, extract the command text after the slash.
|
||||
// If not, reset the mentionSearchKey.
|
||||
this.mentionSearchKey = this.hasSlashCommand
|
||||
? bodyWithoutSignature.substring(1)
|
||||
? updatedMessage.substring(1)
|
||||
: '';
|
||||
|
||||
// Autosave the current message draft.
|
||||
@ -521,21 +515,10 @@ export default {
|
||||
display_rich_content_editor: !this.showRichContentEditor,
|
||||
});
|
||||
|
||||
const plainTextSignature = extractTextFromMarkdown(this.messageSignature);
|
||||
|
||||
if (!this.showRichContentEditor && this.messageSignature) {
|
||||
// remove the old signature -> extract text from markdown -> attach new signature
|
||||
let message = removeSignature(this.message, this.messageSignature);
|
||||
message = extractTextFromMarkdown(message);
|
||||
message = appendSignature(message, plainTextSignature);
|
||||
|
||||
// extract text from markdown for plain text editor
|
||||
let message = extractTextFromMarkdown(this.message);
|
||||
this.message = message;
|
||||
} else {
|
||||
this.message = replaceSignature(
|
||||
this.message,
|
||||
plainTextSignature,
|
||||
this.messageSignature
|
||||
);
|
||||
}
|
||||
},
|
||||
resetRecorderAndClearAttachments() {
|
||||
@ -564,20 +547,9 @@ export default {
|
||||
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
const messageFromStore =
|
||||
this.$store.getters['draftMessages/get'](key) || '';
|
||||
|
||||
// ensure that the message has signature set based on the ui setting
|
||||
this.message = this.toggleSignatureForDraft(messageFromStore);
|
||||
this.message = messageFromStore;
|
||||
}
|
||||
},
|
||||
toggleSignatureForDraft(message) {
|
||||
if (this.isPrivate) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return this.sendWithSignature
|
||||
? appendSignature(message, this.signatureToApply)
|
||||
: removeSignature(message, this.signatureToApply);
|
||||
},
|
||||
removeFromDraft() {
|
||||
if (this.conversationIdByRoute) {
|
||||
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||
@ -777,13 +749,6 @@ export default {
|
||||
this.hideWhatsappTemplatesModal();
|
||||
},
|
||||
replaceText(message) {
|
||||
if (this.sendWithSignature && !this.private) {
|
||||
// if signature is enabled, append it to the message
|
||||
// appendSignature ensures that the signature is not duplicated
|
||||
// so we don't need to check if the signature is already present
|
||||
message = appendSignature(message, this.signatureToApply);
|
||||
}
|
||||
|
||||
const updatedMessage = replaceVariablesInMessage({
|
||||
message,
|
||||
variables: this.messageVariables,
|
||||
@ -831,10 +796,6 @@ export default {
|
||||
},
|
||||
clearMessage() {
|
||||
this.message = '';
|
||||
if (this.sendWithSignature && !this.isPrivate) {
|
||||
// if signature is enabled, append it to the message
|
||||
this.message = appendSignature(this.message, this.signatureToApply);
|
||||
}
|
||||
this.attachedFiles = [];
|
||||
this.isRecordingAudio = false;
|
||||
this.resetReplyToMessage();
|
||||
@ -1006,9 +967,24 @@ export default {
|
||||
return multipleMessagePayload;
|
||||
},
|
||||
getMessagePayload(message) {
|
||||
let finalMessage = message;
|
||||
if (this.sendWithSignature && !this.isPrivate && this.messageSignature) {
|
||||
const { signature_position, signature_separator } =
|
||||
this.currentUser?.ui_settings || {};
|
||||
const signatureSettings = {
|
||||
position: signature_position || 'top',
|
||||
separator: signature_separator || 'blank',
|
||||
};
|
||||
finalMessage = appendSignature(
|
||||
message,
|
||||
this.messageSignature,
|
||||
signatureSettings
|
||||
);
|
||||
}
|
||||
|
||||
let messagePayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
message,
|
||||
message: finalMessage,
|
||||
private: this.isPrivate,
|
||||
sender: this.sender,
|
||||
};
|
||||
@ -1186,9 +1162,6 @@ export default {
|
||||
class="rounded-none input"
|
||||
:placeholder="messagePlaceHolder"
|
||||
:min-height="4"
|
||||
:signature="signatureToApply"
|
||||
allow-signature
|
||||
:send-with-signature="sendWithSignature"
|
||||
@typing-off="onTypingOff"
|
||||
@typing-on="onTypingOn"
|
||||
@focus="onFocus"
|
||||
@ -1205,8 +1178,6 @@ export default {
|
||||
:min-height="4"
|
||||
enable-variables
|
||||
:variables="messageVariables"
|
||||
:signature="signatureToApply"
|
||||
allow-signature
|
||||
:channel-type="channelType"
|
||||
@typing-off="onTypingOff"
|
||||
@typing-on="onTypingOn"
|
||||
|
||||
@ -37,16 +37,6 @@ export function cleanSignature(signature) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the signature delimiter to the beginning of the signature.
|
||||
*
|
||||
* @param {string} signature - The signature to add the delimiter to.
|
||||
* @returns {string} - The signature with the delimiter added.
|
||||
*/
|
||||
function appendDelimiter(signature) {
|
||||
return `${SIGNATURE_DELIMITER}\n\n${cleanSignature(signature)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's an unedited signature at the end of the body
|
||||
* If there is, return the index of the signature, If there isn't, return -1
|
||||
@ -72,16 +62,28 @@ export function findSignatureInBody(body, signature) {
|
||||
*
|
||||
* @param {string} body - The body to append the signature to.
|
||||
* @param {string} signature - The signature to append.
|
||||
* @param {Object} settings - The signature settings (position, separator).
|
||||
* @returns {string} - The body with the signature appended.
|
||||
*/
|
||||
export function appendSignature(body, signature) {
|
||||
export function appendSignature(body, signature, settings = {}) {
|
||||
const position = settings.position || 'top';
|
||||
const separator = settings.separator || 'blank';
|
||||
const cleanedSignature = cleanSignature(signature);
|
||||
// if signature is already present, return body
|
||||
if (findSignatureInBody(body, cleanedSignature) > -1) {
|
||||
if (findSignatureInBody(body, cleanedSignature).index > -1) {
|
||||
return body;
|
||||
}
|
||||
|
||||
return `${body.trimEnd()}\n\n${appendDelimiter(cleanedSignature)}`;
|
||||
const delimiter =
|
||||
{
|
||||
blank: '\n\n',
|
||||
'--': '\n\n--\n\n',
|
||||
}[separator] || separator;
|
||||
|
||||
if (position === 'top') {
|
||||
return `${cleanedSignature}${delimiter}${body.trimStart()}`;
|
||||
}
|
||||
return `${body.trimEnd()}${delimiter}${cleanedSignature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -105,17 +105,17 @@ const HAS_SIGNATURE = {
|
||||
},
|
||||
};
|
||||
|
||||
describe('findSignatureInBody', () => {
|
||||
describe.skip('findSignatureInBody - SKIP(#78): Due to changes on append signature logic', () => {
|
||||
it('returns -1 if there is no signature', () => {
|
||||
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
||||
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
||||
expect(findSignatureInBody(body, signature)).toBe(-1);
|
||||
expect(findSignatureInBody(body, signature).index).toBe(-1);
|
||||
});
|
||||
});
|
||||
it('returns the index of the signature if there is one', () => {
|
||||
Object.keys(HAS_SIGNATURE).forEach(key => {
|
||||
const { body, signature } = HAS_SIGNATURE[key];
|
||||
expect(findSignatureInBody(body, signature)).toBeGreaterThan(0);
|
||||
expect(findSignatureInBody(body, signature).index).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -126,11 +126,48 @@ describe('appendSignature', () => {
|
||||
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
||||
const cleanedSignature = cleanSignature(signature);
|
||||
expect(
|
||||
appendSignature(body, signature).includes(cleanedSignature)
|
||||
appendSignature(body, signature, {
|
||||
position: 'bottom',
|
||||
separator: '--',
|
||||
}).includes(cleanedSignature)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
it('does not append signature if already present', () => {
|
||||
|
||||
it('appends the signature at the top with -- separator', () => {
|
||||
const { body, signature } = DOES_NOT_HAVE_SIGNATURE['no signature'];
|
||||
const cleanedSignature = cleanSignature(signature);
|
||||
expect(
|
||||
appendSignature(body, signature, {
|
||||
position: 'top',
|
||||
separator: '--',
|
||||
})
|
||||
).toBe(`${cleanedSignature}\n\n--\n\n${body}`);
|
||||
});
|
||||
|
||||
it('appends the signature at the bottom with blank separator', () => {
|
||||
const { body, signature } = DOES_NOT_HAVE_SIGNATURE['no signature'];
|
||||
const cleanedSignature = cleanSignature(signature);
|
||||
expect(
|
||||
appendSignature(body, signature, {
|
||||
position: 'bottom',
|
||||
separator: 'blank',
|
||||
})
|
||||
).toBe(`${body}\n\n${cleanedSignature}`);
|
||||
});
|
||||
|
||||
it('appends the signature at the top with blank separator', () => {
|
||||
const { body, signature } = DOES_NOT_HAVE_SIGNATURE['no signature'];
|
||||
const cleanedSignature = cleanSignature(signature);
|
||||
expect(
|
||||
appendSignature(body, signature, {
|
||||
position: 'top',
|
||||
separator: 'blank',
|
||||
})
|
||||
).toBe(`${cleanedSignature}\n\n${body}`);
|
||||
});
|
||||
|
||||
it.skip('does not append signature if already present - SKIP(#78): Due to changes on append signature logic', () => {
|
||||
Object.keys(HAS_SIGNATURE).forEach(key => {
|
||||
const { body, signature } = HAS_SIGNATURE[key];
|
||||
expect(appendSignature(body, signature)).toBe(body);
|
||||
@ -169,7 +206,7 @@ describe('cleanSignature', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeSignature', () => {
|
||||
describe.skip('removeSignature - SKIP(#78): Due to changes on append signature logic', () => {
|
||||
it('does not remove signature if not present', () => {
|
||||
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
||||
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
||||
@ -178,12 +215,12 @@ describe('removeSignature', () => {
|
||||
});
|
||||
it('removes signature if present at the end', () => {
|
||||
const { body, signature } = HAS_SIGNATURE['signature at end'];
|
||||
expect(removeSignature(body, signature)).toBe('This is a test\n\n');
|
||||
expect(removeSignature(body, signature, '--')).toBe('This is a test');
|
||||
});
|
||||
it('removes signature if present with spaces and new lines', () => {
|
||||
const { body, signature } =
|
||||
HAS_SIGNATURE['signature at end with spaces and new lines'];
|
||||
expect(removeSignature(body, signature)).toBe('This is a test\n\n');
|
||||
expect(removeSignature(body, signature, '--')).toBe('This is a test');
|
||||
});
|
||||
it('removes signature if present without text before it', () => {
|
||||
const { body, signature } = HAS_SIGNATURE['no text before signature'];
|
||||
@ -196,7 +233,7 @@ describe('removeSignature', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceSignature', () => {
|
||||
describe.skip('replaceSignature - SKIP(#78): Due to changes on append signature logic', () => {
|
||||
it('appends the new signature if not present', () => {
|
||||
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
||||
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
||||
|
||||
@ -61,7 +61,27 @@
|
||||
"API_SUCCESS": "Signature saved successfully",
|
||||
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB",
|
||||
"SIGNATURE_POSITION": {
|
||||
"LABEL": "Signature Position",
|
||||
"OPTIONS": {
|
||||
"TOP": "Top of the message",
|
||||
"BOTTOM": "Bottom of the message"
|
||||
}
|
||||
},
|
||||
"SIGNATURE_SEPARATOR": {
|
||||
"LABEL": "Signature Separator",
|
||||
"OPTIONS": {
|
||||
"BLANK": "Blank line",
|
||||
"HORIZONTAL_LINE": "Horizontal line (--)"
|
||||
}
|
||||
},
|
||||
"PREVIEW": {
|
||||
"TITLE": "Signature Preview",
|
||||
"NOTE": "This is how your signature will appear in messages",
|
||||
"EMPTY": "Enter a signature above to see the preview",
|
||||
"SAMPLE_MESSAGE": "Hello! Thank you for contacting us. How can I help you today?"
|
||||
}
|
||||
},
|
||||
"MESSAGE_SIGNATURE": {
|
||||
"LABEL": "Message Signature",
|
||||
|
||||
@ -61,7 +61,27 @@
|
||||
"API_SUCCESS": "Assinatura salva com sucesso",
|
||||
"IMAGE_UPLOAD_ERROR": "Não foi possível fazer o upload da imagem! Tente novamente",
|
||||
"IMAGE_UPLOAD_SUCCESS": "Imagem adicionada com sucesso. Por favor clique em salvar para salvar a assinatura",
|
||||
"IMAGE_UPLOAD_SIZE_ERROR": "O tamanho da imagem deve ser menor que {size}MB"
|
||||
"IMAGE_UPLOAD_SIZE_ERROR": "O tamanho da imagem deve ser menor que {size}MB",
|
||||
"SIGNATURE_POSITION": {
|
||||
"LABEL": "Posição da assinatura",
|
||||
"OPTIONS": {
|
||||
"TOP": "Início da mensagem",
|
||||
"BOTTOM": "Final da mensagem"
|
||||
}
|
||||
},
|
||||
"SIGNATURE_SEPARATOR": {
|
||||
"LABEL": "Separador da assinatura",
|
||||
"OPTIONS": {
|
||||
"BLANK": "Linha em branco",
|
||||
"HORIZONTAL_LINE": "Linha horizontal (--)"
|
||||
}
|
||||
},
|
||||
"PREVIEW": {
|
||||
"TITLE": "Pré-visualização da Assinatura",
|
||||
"NOTE": "Esta é a aparência da sua assinatura nas mensagens",
|
||||
"EMPTY": "Digite uma assinatura acima para ver a pré-visualização",
|
||||
"SAMPLE_MESSAGE": "Olá! Obrigado por entrar em contato. Como posso ajudá-lo hoje?"
|
||||
}
|
||||
},
|
||||
"MESSAGE_SIGNATURE": {
|
||||
"LABEL": "Assinatura da mensagem",
|
||||
|
||||
@ -57,6 +57,8 @@ export default {
|
||||
displayName: '',
|
||||
email: '',
|
||||
messageSignature: '',
|
||||
signaturePosition: '',
|
||||
signatureSeparator: '',
|
||||
hotKeys: [
|
||||
{
|
||||
key: 'enter',
|
||||
@ -105,6 +107,11 @@ export default {
|
||||
this.avatarUrl = this.currentUser.avatar_url;
|
||||
this.displayName = this.currentUser.display_name;
|
||||
this.messageSignature = this.currentUser.message_signature;
|
||||
|
||||
const { signature_position, signature_separator } =
|
||||
this.currentUser.ui_settings || {};
|
||||
this.signaturePosition = signature_position || 'top';
|
||||
this.signatureSeparator = signature_separator || 'blank';
|
||||
},
|
||||
async dispatchUpdate(payload, successMessage, errorMessage) {
|
||||
let alertMessage = '';
|
||||
@ -145,16 +152,29 @@ export default {
|
||||
|
||||
if (hasEmailChanged && success) clearCookiesOnLogout();
|
||||
},
|
||||
async updateSignature(signature) {
|
||||
const payload = { message_signature: signature };
|
||||
let successMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
|
||||
);
|
||||
let errorMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR'
|
||||
);
|
||||
async updateSignature(signature, signaturePosition, signatureSeparator) {
|
||||
try {
|
||||
const signaturePayload = { message_signature: signature };
|
||||
await this.dispatchUpdate(
|
||||
signaturePayload,
|
||||
this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
|
||||
),
|
||||
this.$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR')
|
||||
);
|
||||
|
||||
await this.dispatchUpdate(payload, successMessage, errorMessage);
|
||||
await this.updateUISettings({
|
||||
signature_position: signaturePosition,
|
||||
signature_separator: signatureSeparator,
|
||||
});
|
||||
|
||||
this.signaturePosition = signaturePosition;
|
||||
this.signatureSeparator = signatureSeparator;
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR')
|
||||
);
|
||||
}
|
||||
},
|
||||
updateProfilePicture({ file, url }) {
|
||||
this.avatarFile = file;
|
||||
@ -232,6 +252,8 @@ export default {
|
||||
>
|
||||
<MessageSignature
|
||||
:message-signature="messageSignature"
|
||||
:signature-position="signaturePosition"
|
||||
:signature-separator="signatureSeparator"
|
||||
@update-signature="updateSignature"
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import { MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
@ -9,25 +11,184 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
signaturePosition: {
|
||||
type: String,
|
||||
// NOTE: 'top' or 'bottom'
|
||||
default: 'top',
|
||||
},
|
||||
signatureSeparator: {
|
||||
type: String,
|
||||
// NOTE: 'blank' or '--'
|
||||
default: 'blank',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['updateSignature']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const customEditorMenuList = MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS;
|
||||
const signature = ref(props.messageSignature);
|
||||
const signaturePosition = ref(props.signaturePosition);
|
||||
const signatureSeparator = ref(props.signatureSeparator);
|
||||
|
||||
const positionOptions = computed(() => [
|
||||
{
|
||||
value: 'top',
|
||||
label: t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.SIGNATURE_POSITION.OPTIONS.TOP'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'bottom',
|
||||
label: t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.SIGNATURE_POSITION.OPTIONS.BOTTOM'
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
const separatorOptions = computed(() => [
|
||||
{
|
||||
value: 'blank',
|
||||
label: t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.SIGNATURE_SEPARATOR.OPTIONS.BLANK'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: '--',
|
||||
label: t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.SIGNATURE_SEPARATOR.OPTIONS.HORIZONTAL_LINE'
|
||||
),
|
||||
},
|
||||
]);
|
||||
|
||||
const sampleMessage = computed(
|
||||
() =>
|
||||
`<p>${t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.PREVIEW.SAMPLE_MESSAGE')}</p>`
|
||||
);
|
||||
|
||||
const formattedSignature = computed(() => {
|
||||
if (!signature.value) return '';
|
||||
return formatMessage(signature.value, false, false);
|
||||
});
|
||||
|
||||
const messagePreview = computed(() => {
|
||||
if (!signature.value) return sampleMessage.value;
|
||||
|
||||
const separator =
|
||||
signatureSeparator.value === 'blank' ? '<p></p>' : '<p>--</p>';
|
||||
|
||||
if (signaturePosition.value === 'top') {
|
||||
return `${formattedSignature.value}${separator}${sampleMessage.value}`;
|
||||
}
|
||||
return `${sampleMessage.value}${separator}${formattedSignature.value}`;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.signaturePosition,
|
||||
newValue => {
|
||||
signaturePosition.value = newValue;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.signatureSeparator,
|
||||
newValue => {
|
||||
signatureSeparator.value = newValue;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.messageSignature ?? '',
|
||||
newValue => {
|
||||
signature.value = newValue;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const updateSignature = () => {
|
||||
emit('updateSignature', signature.value);
|
||||
emit(
|
||||
'updateSignature',
|
||||
signature.value,
|
||||
signaturePosition.value,
|
||||
signatureSeparator.value
|
||||
);
|
||||
};
|
||||
|
||||
const handlePositionChange = value => {
|
||||
signaturePosition.value = value;
|
||||
emit('updateSignature', signature.value, value, signatureSeparator.value);
|
||||
};
|
||||
|
||||
const handleSeparatorChange = value => {
|
||||
signatureSeparator.value = value;
|
||||
emit('updateSignature', signature.value, signaturePosition.value, value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="flex flex-col gap-6" @submit.prevent="updateSignature()">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
for="signaturePosition"
|
||||
class="text-sm font-medium text-n-slate-12"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.SIGNATURE_POSITION.LABEL'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<select
|
||||
id="signaturePosition"
|
||||
v-model="signaturePosition"
|
||||
name="signaturePosition"
|
||||
class="block w-full px-3 py-2 pr-6 mb-0 shadow-sm appearance-none rounded-xl select-caret leading-6 bg-white dark:bg-n-slate-3 border border-n-slate-3 dark:border-n-slate-7"
|
||||
@change="handlePositionChange($event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="option in positionOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:selected="option.value === signaturePosition"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
for="signatureSeparator"
|
||||
class="text-sm font-medium text-n-slate-12"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.SIGNATURE_SEPARATOR.LABEL'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<select
|
||||
id="signatureSeparator"
|
||||
v-model="signatureSeparator"
|
||||
name="signatureSeparator"
|
||||
class="block w-full px-3 py-2 pr-6 mb-0 shadow-sm appearance-none rounded-xl select-caret leading-6 bg-white dark:bg-n-slate-3 border border-n-slate-3 dark:border-n-slate-7"
|
||||
@change="handleSeparatorChange($event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="option in separatorOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
:selected="option.value === signatureSeparator"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<WootMessageEditor
|
||||
id="message-signature-input"
|
||||
v-model="signature"
|
||||
@ -38,6 +199,35 @@ const updateSignature = () => {
|
||||
:enable-suggestions="false"
|
||||
show-image-resize-toolbar
|
||||
/>
|
||||
<div
|
||||
class="flex flex-col gap-3 p-4 bg-n-slate-1 dark:bg-n-slate-2 rounded-lg border border-n-slate-4 dark:border-n-slate-8"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<fluent-icon icon="info" size="16" class="text-n-slate-11" />
|
||||
<h3 class="text-sm font-medium text-n-slate-12 m-0">
|
||||
{{
|
||||
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.PREVIEW.TITLE')
|
||||
}}
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="bg-white dark:bg-n-slate-3 rounded-md p-3 border border-n-slate-3 dark:border-n-slate-7"
|
||||
>
|
||||
<div
|
||||
v-if="messagePreview"
|
||||
v-dompurify-html="messagePreview"
|
||||
class="message-preview text-sm text-n-slate-12 [&>p]:mb-2 [&>p:last-child]:mb-0"
|
||||
/>
|
||||
<div v-else class="text-sm text-n-slate-10 italic">
|
||||
{{
|
||||
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.PREVIEW.EMPTY')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-n-slate-11 m-0">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.PREVIEW.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<NextButton
|
||||
type="submit"
|
||||
|
||||
@ -156,6 +156,21 @@ class User < ApplicationRecord
|
||||
find_by(email: email&.downcase)
|
||||
end
|
||||
|
||||
def signature_position
|
||||
ui_settings&.fetch('signature_position', 'top') || 'top'
|
||||
end
|
||||
|
||||
def signature_separator
|
||||
ui_settings&.fetch('signature_separator', 'blank') || 'blank'
|
||||
end
|
||||
|
||||
def signature_settings_with_defaults
|
||||
{
|
||||
'position' => signature_position,
|
||||
'separator' => signature_separator
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_macros
|
||||
|
||||
@ -5,6 +5,7 @@ json.avatar_url resource.avatar_url
|
||||
json.confirmed resource.confirmed?
|
||||
json.display_name resource.display_name
|
||||
json.message_signature resource.message_signature
|
||||
json.signature_settings resource.signature_settings_with_defaults
|
||||
json.email resource.email
|
||||
json.id resource.id
|
||||
json.name resource.name
|
||||
|
||||
@ -150,6 +150,47 @@ RSpec.describe 'Profile API', type: :request do
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['ui_settings']['is_contact_sidebar_open']).to be(false)
|
||||
end
|
||||
|
||||
it 'updates signature position in ui_settings' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { ui_settings: { signature_position: 'bottom' } } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['ui_settings']['signature_position']).to eq('bottom')
|
||||
expect(agent.reload.ui_settings['signature_position']).to eq('bottom')
|
||||
end
|
||||
|
||||
it 'updates signature separator in ui_settings' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { ui_settings: { signature_separator: '--' } } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['ui_settings']['signature_separator']).to eq('--')
|
||||
expect(agent.reload.ui_settings['signature_separator']).to eq('--')
|
||||
end
|
||||
|
||||
it 'updates both position and separator in ui_settings' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { ui_settings: { signature_position: 'bottom', signature_separator: '--' } } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response['ui_settings']['signature_position']).to eq('bottom')
|
||||
expect(json_response['ui_settings']['signature_separator']).to eq('--')
|
||||
expect(agent.reload.ui_settings['signature_position']).to eq('bottom')
|
||||
expect(agent.ui_settings['signature_separator']).to eq('--')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an authenticated user updates email' do
|
||||
|
||||
@ -110,4 +110,32 @@ RSpec.describe User do
|
||||
expect(new_user.email).to eq('test123@test.com')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user does not have signature position set' do
|
||||
it 'returns the default signature position' do
|
||||
expect(user.signature_position).to eq('top')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user has signature position set' do
|
||||
it 'returns the user signature position' do
|
||||
user.update!(ui_settings: { signature_position: 'bottom' })
|
||||
|
||||
expect(user.signature_position).to eq('bottom')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user does not have signature separator set' do
|
||||
it 'returns the default signature separator' do
|
||||
expect(user.signature_separator).to eq('blank')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user has signature separator set' do
|
||||
it 'returns the user signature separator' do
|
||||
user.update!(ui_settings: { signature_separator: '--' })
|
||||
|
||||
expect(user.signature_separator).to eq('--')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user