Merge branch 'develop' into feat/app-store-reviews

This commit is contained in:
Muhsin Keloth
2026-05-22 10:55:59 +04:00
committed by GitHub
66 changed files with 230 additions and 235 deletions
@@ -63,10 +63,8 @@ export default {
.colorpicker--chrome.vc-chrome {
@apply shadow-lg -mt-2.5 absolute z-[9999] border border-solid border-n-weak rounded;
::v-deep {
input {
@apply bg-white dark:bg-white;
}
:deep(input) {
@apply bg-white dark:bg-white;
}
}
</style>
@@ -1,6 +1,6 @@
<script setup>
import getUuid from 'widget/helpers/uuid';
import { ref, onMounted, onUnmounted, defineEmits, defineExpose } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import WaveSurfer from 'wavesurfer.js';
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.js';
import { format, intervalToDuration } from 'date-fns';
@@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
import { useElementSize, useWindowSize } from '@vueuse/core';
import { useMapGetter } from 'dashboard/composables/store';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import { useCaptain } from 'dashboard/composables/useCaptain';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
@@ -19,9 +18,11 @@ const props = defineProps({
type: Boolean,
default: false,
},
editorContent: {
type: String,
default: undefined,
// Signature-aware emptiness is computed by the parent (which has access to
// the signature + channel context) and passed in as a boolean.
hasContent: {
type: Boolean,
default: false,
},
conversationId: {
type: Number,
@@ -33,17 +34,8 @@ const emit = defineEmits(['executeCopilotAction']);
const { t } = useI18n();
const { draftMessage } = useCaptain();
const replyMode = useMapGetter('draftMessages/getReplyEditorMode');
// When editorContent prop is passed, use it exclusively (even if empty)
// This ensures each editor instance shows menu items based on its own content
// Falls back to global draftMessage only when editorContent is not provided
const effectiveContent = computed(() =>
props.editorContent !== undefined ? props.editorContent : draftMessage.value
);
// Selection-based menu items (when text is selected)
const menuItems = computed(() => {
const items = [];
@@ -63,7 +55,7 @@ const menuItems = computed(() => {
} else if (
props.conversationId &&
replyMode.value === REPLY_EDITOR_MODES.REPLY &&
effectiveContent.value
props.hasContent
) {
items.push({
label: t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.IMPROVE_REPLY'),
@@ -72,7 +64,7 @@ const menuItems = computed(() => {
});
}
if (effectiveContent.value) {
if (props.hasContent) {
items.push(
{
label: t(
@@ -354,16 +354,17 @@ 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
// Use effective channel type to match how signature was appended
const bodyWithoutSignature = props.signature
? removeSignatureHelper(
content,
props.signature,
effectiveChannelType.value
)
: content;
// Only strip the signature when it's actually being auto-appended for this
// draft. Otherwise an agent whose typed text happens to match their saved
// signature would be mistakenly treated as empty.
const bodyWithoutSignature =
sendWithSignature.value && props.signature
? removeSignatureHelper(
content,
props.signature,
effectiveChannelType.value
)
: content;
// trimming should remove all the whitespaces, so we can check the length
return bodyWithoutSignature.trim().length === 0;
@@ -474,17 +475,6 @@ function removeSignature() {
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();
@@ -559,6 +549,20 @@ function emitOnChange() {
emit('update:modelValue', contentFromEditor());
}
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();
}
// reloadState replaces editor state directly and bypasses dispatchTransaction,
// so v-model never hears about the signature change — sync it back explicitly.
emitOnChange();
}
function updateImgToolbarOnDelete() {
// check if the selected node is present or not on keyup
// this is needed because the user can select an image and then delete it
@@ -899,7 +903,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
v-on-click-outside="handleClickOutside"
:has-selection="isTextSelected"
:is-editor-menu-popover="isEditorMenuPopover"
:editor-content="modelValue"
:has-content="!isBodyEmpty(modelValue)"
:conversation-id="conversationId"
:show-selection-menu="showSelectionMenu"
:show-general-menu="false"
@@ -419,7 +419,7 @@ export default {
@apply flex;
}
::v-deep .file-uploads {
:deep(.file-uploads) {
label {
@apply cursor-pointer;
}
@@ -53,6 +53,10 @@ export default {
type: String,
default: undefined,
},
hasContent: {
type: Boolean,
default: false,
},
},
emits: ['setReplyMode', 'toggleEditorSize', 'executeCopilotAction'],
setup(props, { emit }) {
@@ -76,6 +80,7 @@ export default {
const { captainTasksEnabled } = useCaptain();
const showCopilotMenu = ref(false);
const copilotToggleRef = ref(null);
const handleCopilotAction = (actionKey, data) => {
emit('executeCopilotAction', actionKey, data || props.editorContent);
@@ -117,6 +122,7 @@ export default {
captainTasksEnabled,
handleCopilotAction,
showCopilotMenu,
copilotToggleRef,
toggleCopilotMenu,
handleClickOutside,
};
@@ -164,6 +170,7 @@ export default {
<div v-if="captainTasksEnabled" class="flex items-center gap-2">
<div class="relative">
<NextButton
ref="copilotToggleRef"
ghost
:disabled="disabled || isEditorDisabled"
:class="{
@@ -176,9 +183,12 @@ export default {
/>
<CopilotMenuBar
v-if="showCopilotMenu"
v-on-click-outside="handleClickOutside"
v-on-click-outside="[
handleClickOutside,
{ ignore: [copilotToggleRef] },
]"
:has-selection="false"
:editor-content="editorContent"
:has-content="hasContent"
:conversation-id="conversationId"
class="ltr:right-0 rtl:left-0 bottom-full mb-2"
@execute-copilot-action="handleCopilotAction"
@@ -179,6 +179,21 @@ export default {
}
return true;
},
hasMeaningfulEditorContent() {
const body = this.message || '';
// Only strip the signature when it's actually being auto-appended.
// If the toggle is off, the agent's text might happen to match their
// saved signature and we'd incorrectly treat it as empty.
const shouldStripSignature =
!this.isPrivate && this.sendWithSignature && !!this.messageSignature;
if (!shouldStripSignature) return !!body.trim();
const stripped = removeSignature(
body,
this.messageSignature,
getEffectiveChannelType(this.channelType, this.inbox?.medium || '')
);
return !!stripped.trim();
},
isReplyRestricted() {
return (
!this.currentChat?.can_reply &&
@@ -1231,6 +1246,7 @@ export default {
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
:editor-content="message"
:has-content="hasMeaningfulEditorContent"
@set-reply-mode="setReplyMode"
@toggle-editor-size="toggleEditorSize"
@toggle-copilot="copilot.toggleEditor"
@@ -1,5 +1,5 @@
<script setup>
import { ref, computed, defineOptions } from 'vue';
import { ref, computed } from 'vue';
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
@@ -121,7 +121,7 @@ const variableKey = (item = {}) => {
}
}
.canned-item__button::v-deep .button__content {
.canned-item__button :deep(.button__content) {
@apply overflow-hidden text-ellipsis whitespace-nowrap;
}
</style>