Shivam Mishra
2024-10-02 13:06:30 +05:30
committed by GitHub
parent e0bf2bd9d4
commit 42f6621afb
661 changed files with 15939 additions and 31194 deletions

View File

@@ -9,6 +9,7 @@ import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue';
import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
import { emitter } from 'shared/helpers/mitt';
export default {
components: {
@@ -80,7 +81,7 @@ export default {
},
mounted() {
this.$emitter.on(CMD_AI_ASSIST, this.onAIAssist);
emitter.on(CMD_AI_ASSIST, this.onAIAssist);
this.initializeMessage(this.draftMessage);
},
@@ -124,7 +125,7 @@ export default {
<div v-if="isAIIntegrationEnabled" class="relative">
<AIAssistanceCTAButton
v-if="shouldShowAIAssistCTAButton"
@click="openAIAssist"
@open="openAIAssist"
/>
<woot-button
v-else

View File

@@ -2,7 +2,7 @@
export default {
methods: {
onClick() {
this.$emit('click');
this.$emit('open');
},
},
};

View File

@@ -9,7 +9,7 @@ export default {
WootMessageEditor,
},
props: {
value: {
modelValue: {
type: Object,
default: () => null,
},
@@ -41,21 +41,23 @@ export default {
computed: {
action_name: {
get() {
if (!this.value) return null;
return this.value.action_name;
if (!this.modelValue) return null;
return this.modelValue.action_name;
},
set(value) {
const payload = this.value || {};
const payload = this.modelValue || {};
this.$emit('update:modelValue', { ...payload, action_name: value });
this.$emit('input', { ...payload, action_name: value });
},
},
action_params: {
get() {
if (!this.value) return null;
return this.value.action_params;
if (!this.modelValue) return null;
return this.modelValue.action_params;
},
set(value) {
const payload = this.value || {};
const payload = this.modelValue || {};
this.$emit('update:modelValue', { ...payload, action_params: value });
this.$emit('input', { ...payload, action_params: value });
},
},

View File

@@ -40,7 +40,7 @@ export default {
:max-height="160"
:options="teams"
:allow-empty="false"
@input="updateValue"
@update:model-value="updateValue"
/>
<textarea
v-model="message"

View File

@@ -33,7 +33,7 @@ export default {
'automations/uploadAttachment',
file
);
this.$emit('input', [id]);
this.$emit('update:modelValue', [id]);
this.uploadState = 'uploaded';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADED');
} catch (error) {

View File

@@ -52,8 +52,9 @@ useKeyboardEvents(keyboardEvents);
@change="onTabChange"
>
<woot-tabs-item
v-for="item in items"
v-for="(item, index) in items"
:key="item.key"
:index="index"
:name="item.name"
:count="item.count"
/>

View File

@@ -1,12 +1,12 @@
<script>
import { Chrome } from 'vue-color';
import { Chrome } from '@lk77/vue3-color';
export default {
components: {
Chrome,
},
props: {
value: {
modelValue: {
type: String,
default: '',
},
@@ -26,7 +26,7 @@ export default {
this.isPickerOpen = !this.isPickerOpen;
},
updateColor(e) {
this.$emit('input', e.hex);
this.$emit('update:modelValue', e.hex);
},
},
};
@@ -36,23 +36,23 @@ export default {
<div class="colorpicker">
<div
class="colorpicker--selected"
:style="`background-color: ${value}`"
:style="`background-color: ${modelValue}`"
@click.prevent="toggleColorPicker"
/>
<Chrome
v-if="isPickerOpen"
v-on-clickaway="closeTogglePicker"
disable-alpha
:value="value"
:model-value="modelValue"
class="colorpicker--chrome"
@input="updateColor"
@update:modelValue="updateColor"
/>
</div>
</template>
<style scoped lang="scss">
@import '~dashboard/assets/scss/variables';
@import '~dashboard/assets/scss/mixins';
@import 'dashboard/assets/scss/variables';
@import 'dashboard/assets/scss/mixins';
.colorpicker {
position: relative;

View File

@@ -1,9 +1,10 @@
<script>
export default {
name: 'FilterInput',
props: {
value: {
modelValue: {
type: Object,
default: () => null,
default: () => {},
},
filterAttributes: {
type: Array,
@@ -49,42 +50,42 @@ export default {
computed: {
attributeKey: {
get() {
if (!this.value) return null;
return this.value.attribute_key;
if (!this.modelValue) return null;
return this.modelValue.attribute_key;
},
set(value) {
const payload = this.value || {};
this.$emit('input', { ...payload, attribute_key: value });
const payload = this.modelValue || {};
this.$emit('update:modelValue', { ...payload, attribute_key: value });
},
},
filterOperator: {
get() {
if (!this.value) return null;
return this.value.filter_operator;
if (!this.modelValue) return null;
return this.modelValue.filter_operator;
},
set(value) {
const payload = this.value || {};
this.$emit('input', { ...payload, filter_operator: value });
const payload = this.modelValue || {};
this.$emit('update:modelValue', { ...payload, filter_operator: value });
},
},
values: {
get() {
if (!this.value) return null;
return this.value.values;
if (!this.modelValue) return null;
return this.modelValue.values;
},
set(value) {
const payload = this.value || {};
this.$emit('input', { ...payload, values: value });
const payload = this.modelValue || {};
this.$emit('update:modelValue', { ...payload, values: value });
},
},
query_operator: {
get() {
if (!this.value) return null;
return this.value.query_operator;
if (!this.modelValue) return null;
return this.modelValue.query_operator;
},
set(value) {
const payload = this.value || {};
this.$emit('input', { ...payload, query_operator: value });
const payload = this.modelValue || {};
this.$emit('update:modelValue', { ...payload, query_operator: value });
},
},
custom_attribute_type: {
@@ -93,8 +94,8 @@ export default {
return this.customAttributeType;
},
set() {
const payload = this.value || {};
this.$emit('input', {
const payload = this.modelValue || {};
this.$emit('update:modelValue', {
...payload,
custom_attribute_type: this.customAttributeType,
});
@@ -109,9 +110,9 @@ export default {
value === 'contact_attribute'
) {
// eslint-disable-next-line vue/no-mutating-props
this.value.custom_attribute_type = this.customAttributeType;
this.modelValue.custom_attribute_type = this.customAttributeType;
// eslint-disable-next-line vue/no-mutating-props
} else this.value.custom_attribute_type = '';
} else this.modelValue.custom_attribute_type = '';
},
immediate: true,
},
@@ -155,6 +156,7 @@ export default {
v-for="attribute in group.attributes"
:key="attribute.key"
:value="attribute.key"
:selected="true"
>
{{ attribute.name }}
</option>

View File

@@ -1,37 +0,0 @@
import InboxDropdownItem from './InboxDropdownItem';
export default {
title: 'Components/DropDowns/InboxDropdownItem',
component: InboxDropdownItem,
argTypes: {
name: {
defaultValue: 'My new inbox',
control: {
type: 'text',
},
},
inboxIdentifier: {
defaultValue: 'nithin@mail.com',
control: {
type: 'text',
},
},
channelType: {
defaultValue: 'email',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { InboxDropdownItem },
template: '<inbox-dropdown-item v-bind="$props" ></inbox-dropdown-item>',
});
export const Banner = Template.bind({});
Banner.args = {};

View File

@@ -5,7 +5,7 @@ export default {
props: {
inbox: {
type: Object,
default: () => {},
default: () => { },
},
},
computed: {
@@ -20,7 +20,7 @@ export default {
<template>
<div
class="inbox--name inline-flex items-center py-0.5 px-0 leading-3 whitespace-nowrap font-medium bg-none text-slate-600 dark:text-slate-500 text-xs my-0 mx-2.5"
class="inbox--name inline-flex items-center py-0.5 px-0 leading-3 whitespace-nowrap bg-none text-slate-600 dark:text-slate-500 text-xs my-0 mx-2.5"
>
<fluent-icon
class="mr-0.5 rtl:ml-0.5 rtl:mr-0"

View File

@@ -1,67 +0,0 @@
import { action } from '@storybook/addon-actions';
import LabelSelector from './LabelSelector';
export default {
title: 'Components/Label/Contact Label',
component: LabelSelector,
argTypes: {
contactId: {
control: {
type: 'text ,number',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { LabelSelector },
template:
'<label-selector v-bind="$props" @add="onAdd" @remove="onRemove"></label-selector>',
});
export const ContactLabel = Template.bind({});
ContactLabel.args = {
onAdd: action('Added'),
onRemove: action('Removed'),
allLabels: [
{
id: '1',
title: 'sales',
description: '',
color: '#0a5dd1',
},
{
id: '2',
title: 'refund',
description: '',
color: '#8442f5',
},
{
id: '3',
title: 'testing',
description: '',
color: '#f542f5',
},
{
id: '4',
title: 'scheduled',
description: '',
color: '#42d1f5',
},
],
savedLabels: [
{
id: '2',
title: 'refund',
description: '',
color: '#8442f5',
},
{
id: '4',
title: 'scheduled',
description: '',
color: '#42d1f5',
},
],
};

View File

@@ -69,7 +69,7 @@ useKeyboardEvents(keyboardEvents);
show-close
:color="label.color"
variant="smooth"
@click="removeItem"
@remove="removeItem"
/>
<div class="absolute w-full top-7">
<div

View File

@@ -1,30 +0,0 @@
import SettingIntroBanner from './SettingIntroBanner';
export default {
title: 'Components/Settings/Banner',
component: SettingIntroBanner,
argTypes: {
headerTitle: {
defaultValue: 'Acme Support',
control: {
type: 'text',
},
},
headerContent: {
defaultValue:
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { SettingIntroBanner },
template: '<setting-intro-banner v-bind="$props" ></setting-intro-banner>',
});
export const Banner = Template.bind({});
Banner.args = {};

View File

@@ -1,6 +1,6 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
const props = defineProps({
text: {

View File

@@ -1,61 +0,0 @@
import Thumbnail from './Thumbnail.vue';
export default {
title: 'Components/Thumbnail',
component: Thumbnail,
argTypes: {
src: {
control: {
type: 'text',
},
},
size: {
control: {
type: 'text',
},
},
badge: {
control: {
type: 'select',
options: ['fb', 'whatsapp', 'sms', 'twitter-tweet', 'twitter-dm'],
},
},
variant: {
control: {
type: 'select',
options: ['circle', 'square'],
},
},
username: {
defaultValue: 'John Doe',
control: {
type: 'text',
},
},
status: {
defaultValue: 'circle',
control: {
type: 'select',
options: ['online', 'busy'],
},
},
hasBorder: {
control: {
type: 'boolean',
},
},
shouldShowStatusAlways: {
control: {
type: 'boolean',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { Thumbnail },
template: '<thumbnail v-bind="$props" @click="onClick">{{label}}</thumbnail>',
});
export const Primary = Template.bind({});

View File

@@ -1,25 +1,20 @@
<script>
<script setup>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
export default {
components: {
Thumbnail,
defineProps({
user: {
type: Object,
default: () => {},
},
props: {
user: {
type: Object,
default: () => {},
},
size: {
type: String,
default: '20px',
},
textClass: {
type: String,
default: 'text-xs text-slate-600',
},
size: {
type: String,
default: '20px',
},
};
textClass: {
type: String,
default: 'text-xs text-slate-600',
},
});
</script>
<template>

View File

@@ -1,263 +1,116 @@
<script>
<script setup>
import getUuid from 'widget/helpers/uuid';
import 'video.js/dist/video-js.css';
import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js';
import { useAlert } from 'dashboard/composables';
import Recorder from 'opus-recorder';
// Workers to record Audio .ogg and .wav
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
import waveWorker from 'opus-recorder/dist/waveWorker.min';
import { ref, onMounted, onUnmounted, defineEmits, defineExpose } from 'vue';
import WaveSurfer from 'wavesurfer.js';
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
import RecordPlugin from 'wavesurfer.js/dist/plugins/record.js';
import { format, intervalToDuration } from 'date-fns';
import { convertAudio } from './utils/mp3ConversionUtils';
import 'videojs-wavesurfer/dist/videojs.wavesurfer.js';
import 'videojs-record/dist/videojs.record.js';
import OpusRecorderEngine from 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
import { format, addSeconds } from 'date-fns';
import { AUDIO_FORMATS } from 'shared/constants/messages';
import { convertWavToMp3 } from './utils/mp3ConversionUtils';
WaveSurfer.microphone = MicrophonePlugin;
const RECORDER_CONFIG = {
[AUDIO_FORMATS.WAV]: {
audioMimeType: 'audio/wav',
audioWorkerURL: waveWorker,
},
[AUDIO_FORMATS.MP3]: {
audioMimeType: 'audio/wav',
audioWorkerURL: waveWorker,
},
[AUDIO_FORMATS.OGG]: {
audioMimeType: 'audio/ogg',
audioWorkerURL: encoderWorker,
const props = defineProps({
audioRecordFormat: {
type: String,
required: true,
},
});
const emit = defineEmits(['recorderProgressChanged', 'finishRecord']);
const waveformContainer = ref(null);
const wavesurfer = ref(null);
const record = ref(null);
const isRecording = ref(false);
const isPlaying = ref(false);
const hasRecording = ref(false);
const formatTimeProgress = time => {
const duration = intervalToDuration({ start: 0, end: time });
return format(
new Date(0, 0, 0, 0, duration.minutes, duration.seconds),
'mm:ss'
);
};
export default {
name: 'WootAudioRecorder',
props: {
audioRecordFormat: {
type: String,
default: AUDIO_FORMATS.WAV,
},
},
data() {
return {
player: false,
recordingDateStarted: new Date(0),
initialTimeDuration: '00:00',
recorderOptions: {
controls: true,
bigPlayButton: false,
fluid: false,
controlBar: {
deviceButton: false,
fullscreenToggle: false,
cameraButton: false,
volumePanel: false,
},
plugins: {
wavesurfer: {
backend: 'WebAudio',
waveColor: '#1f93ff',
progressColor: 'rgb(25, 118, 204)',
cursorColor: 'rgba(43, 51, 63, 0.7)',
backgroundColor: 'none',
barWidth: 1,
cursorWidth: 1,
hideScrollbar: true,
plugins: [
WaveSurfer.microphone.create({
bufferSize: 4096,
numberOfInputChannels: 1,
numberOfOutputChannels: 1,
constraints: {
video: false,
audio: true,
},
}),
],
},
record: {
audio: true,
video: false,
maxLength: 900,
timeSlice: 1000,
maxFileSize: 15 * 1024 * 1024,
displayMilliseconds: false,
audioChannels: 1,
audioSampleRate: 48000,
audioBitRate: 128,
audioEngine: 'opus-recorder',
...RECORDER_CONFIG[this.audioRecordFormat],
},
},
},
};
},
computed: {
isRecording() {
return this.player && this.player.record().isRecording();
},
},
mounted() {
window.Recorder = Recorder;
this.fireProgressRecord(this.initialTimeDuration);
this.player = videojs('#audio-wave', this.recorderOptions, () => {
this.$nextTick(() => {
this.player.record().getDevice();
});
const initWaveSurfer = () => {
wavesurfer.value = WaveSurfer.create({
container: waveformContainer.value,
waveColor: '#1F93FF',
progressColor: '#6E6F73',
height: 100,
barWidth: 2,
barGap: 1,
barRadius: 2,
plugins: [
RecordPlugin.create({
scrollingWaveform: true,
renderRecordedAudio: false,
}),
],
});
record.value = wavesurfer.value.plugins[0];
wavesurfer.value.on('finish', () => {
isPlaying.value = false;
});
record.value.on('record-end', async blob => {
const audioUrl = URL.createObjectURL(blob);
const audioBlob = await convertAudio(blob, props.audioRecordFormat);
const fileName = `${getUuid()}.mp3`;
const file = new File([audioBlob], fileName, {
type: props.audioRecordFormat,
});
this.player.on('deviceReady', this.deviceReady);
this.player.on('deviceError', this.deviceError);
this.player.on('startRecord', this.startRecord);
this.player.on('stopRecord', this.stopRecord);
this.player.on('progressRecord', this.progressRecord);
this.player.on('finishRecord', this.finishRecord);
this.player.on('playbackFinish', this.playbackFinish);
},
beforeDestroy() {
if (this.player) {
this.player.dispose();
}
if (window.Recorder) {
window.Recorder = undefined;
}
},
methods: {
deviceReady() {
if (this.player.record().engine instanceof OpusRecorderEngine) {
if (
[AUDIO_FORMATS.WAV, AUDIO_FORMATS.MP3].includes(
this.audioRecordFormat
)
) {
this.player.record().engine.audioType = 'audio/wav';
}
}
this.player.record().start();
},
startRecord() {
this.fireStateRecorderChanged('recording');
},
stopRecord() {
this.fireStateRecorderChanged('stopped');
},
async finishRecord() {
let recordedContent = this.player.recordedData;
let fileName = this.player.recordedData.name;
let type = this.player.recordedData.type;
if (this.audioRecordFormat === AUDIO_FORMATS.MP3) {
recordedContent = await convertWavToMp3(this.player.recordedData);
fileName = `${getUuid()}.mp3`;
type = AUDIO_FORMATS.MP3;
}
const file = new File([recordedContent], fileName, { type });
this.fireRecorderBlob(file);
},
progressRecord() {
this.fireProgressRecord(this.formatTimeProgress());
},
stopAudioRecording() {
this.player.record().stop();
},
deviceError() {
const deviceError = this.player.deviceErrorCode;
const deviceErrorName = deviceError?.name.toLowerCase();
if (
deviceErrorName?.includes('notallowederror') ||
deviceErrorName?.includes('permissiondeniederror')
) {
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION'));
this.fireStateRecorderChanged('notallowederror');
} else {
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR'));
}
},
formatTimeProgress() {
return format(
addSeconds(
new Date(this.recordingDateStarted.getTimezoneOffset() * 1000 * 60),
this.player.record().getDuration()
),
'mm:ss'
);
},
playPause() {
if (this.player.wavesurfer().surfer.isPlaying()) {
this.fireStateRecorderChanged('paused');
} else {
this.fireStateRecorderChanged('playing');
}
this.player.wavesurfer().surfer.playPause();
},
play() {
this.fireStateRecorderChanged('playing');
this.player.wavesurfer().play();
},
pause() {
this.fireStateRecorderChanged('paused');
this.player.wavesurfer().pause();
},
playbackFinish() {
this.fireStateRecorderChanged('paused');
this.player.wavesurfer().pause();
},
fireRecorderBlob(blob) {
this.$emit('finishRecord', {
name: blob.name,
type: blob.type,
size: blob.size,
file: blob,
});
},
fireStateRecorderChanged(state) {
this.$emit('stateRecorderChanged', state);
},
fireProgressRecord(duration) {
this.$emit('stateRecorderProgressChanged', duration);
},
},
wavesurfer.value.load(audioUrl);
emit('finishRecord', {
name: file.name,
type: file.type,
size: file.size,
file,
});
hasRecording.value = true;
isRecording.value = false;
});
record.value.on('record-progress', time => {
emit('recorderProgressChanged', formatTimeProgress(time));
});
};
const stopRecording = () => {
if (isRecording.value) {
record.value.stopRecording();
isRecording.value = false;
}
};
const startRecording = () => {
record.value.startRecording();
isRecording.value = true;
};
const playPause = () => {
if (hasRecording.value) {
wavesurfer.value.playPause();
isPlaying.value = !isPlaying.value;
}
};
onMounted(() => {
initWaveSurfer();
startRecording();
});
onUnmounted(() => {
if (wavesurfer.value) {
wavesurfer.value.destroy();
}
});
defineExpose({ playPause, stopRecording });
</script>
<template>
<div class="audio-wave-wrapper">
<audio id="audio-wave" class="video-js vjs-fill vjs-default-skin" />
<div class="w-full">
<div ref="waveformContainer" />
</div>
</template>
<style lang="scss">
.audio-wave-wrapper {
@apply h-20 min-h-[5rem];
.video-js {
@apply bg-transparent max-h-60 min-h-[3rem] pt-4 px-0 pb-0 resize-none;
}
}
// Added to override the default text and bg style to support dark and light mode.
.video-js .vjs-control-bar,
.vjs-record.video-js .vjs-control.vjs-record-indicator:before {
@apply text-slate-600 dark:text-slate-200 bg-transparent dark:bg-transparent;
}
// Added to fix div overlays the screen and takes over the button clicks
// https://github.com/collab-project/videojs-record/issues/688
// https://github.com/collab-project/videojs-record/pull/709
.vjs-record.video-js .vjs-control.vjs-record-indicator.vjs-hidden,
.vjs-record.video-js .vjs-control.vjs-record-indicator,
.vjs-record.video-js .vjs-control.vjs-record-indicator:before,
.vjs-record.video-js .vjs-control.vjs-record-indicator:after {
@apply pointer-events-none;
}
</style>

View File

@@ -36,10 +36,13 @@ const createState = (
});
};
let editorView = null;
let state;
export default {
mixins: [keyboardEventListenerMixins],
props: {
value: { type: String, default: '' },
modelValue: { type: String, default: '' },
editorId: { type: String, default: '' },
placeholder: { type: String, default: '' },
enabledMenuOptions: { type: Array, default: () => [] },
@@ -54,21 +57,19 @@ export default {
},
data() {
return {
editorView: null,
state: undefined,
plugins: [imagePastePlugin(this.handleImageUpload)],
};
},
computed: {
contentFromEditor() {
if (this.editorView) {
return ArticleMarkdownSerializer.serialize(this.editorView.state.doc);
if (editorView) {
return ArticleMarkdownSerializer.serialize(editorView.state.doc);
}
return '';
},
},
watch: {
value(newValue = '') {
modelValue(newValue = '') {
if (newValue !== this.contentFromEditor) {
this.reloadState();
}
@@ -79,8 +80,8 @@ export default {
},
created() {
this.state = createState(
this.value,
state = createState(
this.modelValue,
this.placeholder,
this.plugins,
{ onImageUpload: this.openFileBrowser },
@@ -90,7 +91,7 @@ export default {
mounted() {
this.createEditorView();
this.editorView.updateState(this.state);
editorView.updateState(state);
this.focusEditorInputField();
},
methods: {
@@ -145,53 +146,48 @@ export default {
}
},
onImageUploadStart(fileUrl) {
const { selection } = this.editorView.state;
const { selection } = editorView.state;
const from = selection.from;
const node = this.editorView.state.schema.nodes.image.create({
const node = editorView.state.schema.nodes.image.create({
src: fileUrl,
});
const paragraphNode = this.editorView.state.schema.node('paragraph');
const paragraphNode = editorView.state.schema.node('paragraph');
if (node) {
// Insert the image and the caption wrapped inside a paragraph
const tr = this.editorView.state.tr
const tr = editorView.state.tr
.replaceSelectionWith(paragraphNode)
.insert(from + 1, node);
this.editorView.dispatch(tr.scrollIntoView());
editorView.dispatch(tr.scrollIntoView());
this.focusEditorInputField();
}
},
reloadState() {
this.state = createState(
this.value,
state = createState(
this.modelValue,
this.placeholder,
this.plugins,
{ onImageUpload: this.openFileBrowser },
this.enabledMenuOptions
);
this.editorView.updateState(this.state);
editorView.updateState(state);
this.focusEditorInputField();
},
createEditorView() {
this.editorView = new EditorView(this.$refs.editor, {
state: this.state,
editorView = new EditorView(this.$refs.editor, {
state: state,
dispatchTransaction: tx => {
this.state = this.state.apply(tx);
this.emitOnChange();
state = state.apply(tx);
editorView.updateState(state);
if (tx.docChanged) {
this.emitOnChange();
}
},
handleDOMEvents: {
keyup: () => {
this.onKeyup();
},
keydown: (view, event) => {
this.onKeydown(event);
},
focus: () => {
this.onFocus();
},
blur: () => {
this.onBlur();
},
keyup: this.onKeyup,
focus: this.onFocus,
blur: this.onBlur,
keydown: this.onKeydown,
paste: (view, event) => {
const data = event.clipboardData.files;
if (data.length > 0) {
@@ -207,22 +203,18 @@ export default {
},
});
},
handleKeyEvents() {},
focusEditorInputField() {
const { tr } = this.editorView.state;
const { tr } = editorView.state;
const selection = Selection.atEnd(tr.doc);
this.editorView.dispatch(tr.setSelection(selection));
this.editorView.focus();
editorView.dispatch(tr.setSelection(selection));
editorView.focus();
},
emitOnChange() {
this.editorView.updateState(this.state);
this.$emit('update:modelValue', this.contentFromEditor);
this.$emit('input', this.contentFromEditor);
},
onKeyup() {
this.$emit('keyup');
},
@@ -255,7 +247,7 @@ export default {
</template>
<style lang="scss">
@import '~@chatwoot/prosemirror-schema/src/styles/article.scss';
@import '@chatwoot/prosemirror-schema/src/styles/article.scss';
.ProseMirror-menubar-wrapper {
display: flex;

View File

@@ -26,7 +26,7 @@ export default {
},
onSend: {
type: Function,
default: () => {},
default: () => { },
},
sendButtonText: {
type: String,
@@ -53,19 +53,19 @@ export default {
},
onFileUpload: {
type: Function,
default: () => {},
default: () => { },
},
toggleEmojiPicker: {
type: Function,
default: () => {},
default: () => { },
},
toggleAudioRecorder: {
type: Function,
default: () => {},
default: () => { },
},
toggleAudioRecorderPlayPause: {
type: Function,
default: () => {},
default: () => { },
},
isRecordingAudio: {
type: Boolean,
@@ -174,9 +174,8 @@ export default {
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.VOICE_RECORDER
) &&
this.showAudioRecorder &&
!isSafari
) && this.showAudioRecorder
// !isSafari
);
},
showAudioPlayStopButton() {

View File

@@ -1,6 +1,7 @@
<script>
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
export default {
name: 'ReplyTopPanel',
props: {
@@ -106,7 +107,7 @@ export default {
icon="dismiss"
color-scheme="secondary"
class-names="popout-button"
@click="$emit('click')"
@click="$emit('togglePopout')"
/>
<woot-button
v-else
@@ -114,7 +115,7 @@ export default {
icon="resize-large"
color-scheme="secondary"
class-names="popout-button"
@click="$emit('click')"
@click="$emit('togglePopout')"
/>
</div>
</template>

View File

@@ -10,7 +10,7 @@ const props = defineProps({
},
});
const emit = defineEmits(['click']);
const emit = defineEmits(['selectEmoji']);
const allEmojis = shallowRef([]);
@@ -34,7 +34,7 @@ function loadEmojis() {
}
function handleMentionClick(item = {}) {
emit('click', item.emoji);
emit('selectEmoji', item.emoji);
}
onMounted(() => {

View File

@@ -1,14 +1,77 @@
import lamejs from 'lamejs';
import lamejs from '@breezystack/lamejs';
const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
const bufferToWav = async (buffer, numChannels, sampleRate) => {
const length = buffer.length * numChannels * 2;
const wav = new ArrayBuffer(44 + length);
const view = new DataView(wav);
// WAV Header
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + length, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * numChannels * 2, true);
view.setUint16(32, numChannels * 2, true);
view.setUint16(34, 16, true);
writeString(view, 36, 'data');
view.setUint32(40, length, true);
// WAV Data
const offset = 44;
for (let i = 0; i < buffer.length; i++) {
for (let channel = 0; channel < numChannels; channel++) {
const sample = Math.max(
-1,
Math.min(1, buffer.getChannelData(channel)[i])
);
view.setInt16(
offset + (i * numChannels + channel) * 2,
sample * 0x7fff,
true
);
}
}
return new Blob([wav], { type: 'audio/wav' });
};
const decodeAudioData = async audioBlob => {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const arrayBuffer = await audioBlob.arrayBuffer();
const audioData = await audioContext.decodeAudioData(arrayBuffer);
return audioData;
};
export const convertToWav = async audioBlob => {
const audioBuffer = await decodeAudioData(audioBlob);
return bufferToWav(
audioBuffer,
audioBuffer.numberOfChannels,
audioBuffer.sampleRate
);
};
/**
* Encodes a mono channel audio stream to MP3 format.
* Encodes audio samples to MP3 format.
* @param {number} channels - Number of audio channels.
* @param {number} sampleRate - Sample rate in Hz.
* @param {Int16Array} samples - Audio samples to be encoded.
* @param {number} bitrate - MP3 bitrate (default: 128)
* @returns {Blob} - The MP3 encoded audio as a Blob.
*/
export const encodeToMP3 = (channels, sampleRate, samples) => {
export const encodeToMP3 = (channels, sampleRate, samples, bitrate = 128) => {
const outputBuffer = [];
const encoder = new lamejs.Mp3Encoder(channels, sampleRate, 128);
const encoder = new lamejs.Mp3Encoder(channels, sampleRate, bitrate);
const maxSamplesPerFrame = 1152;
for (let offset = 0; offset < samples.length; offset += maxSamplesPerFrame) {
@@ -30,24 +93,51 @@ export const encodeToMP3 = (channels, sampleRate, samples) => {
};
/**
* Converts a WAV audio Blob to an MP3 format Blob.
* @param {Blob} blob - The audio data in WAV format as a Blob.
* Converts an audio Blob to an MP3 format Blob.
* @param {Blob} audioBlob - The audio data as a Blob.
* @param {number} bitrate - MP3 bitrate (default: 128)
* @returns {Promise<Blob>} - A Blob containing the MP3 encoded audio.
*/
export const convertWavToMp3 = async blob => {
export const convertToMp3 = async (audioBlob, bitrate = 128) => {
try {
const audioBuffer = await blob.arrayBuffer();
const wavHeader = lamejs.WavHeader.readHeader(new DataView(audioBuffer));
const audioBuffer = await decodeAudioData(audioBlob);
const samples = new Int16Array(
audioBuffer,
wavHeader.dataOffset,
wavHeader.dataLen / 2
audioBuffer.length * audioBuffer.numberOfChannels
);
let offset = 0;
for (let i = 0; i < audioBuffer.length; i += 1) {
for (
let channel = 0;
channel < audioBuffer.numberOfChannels;
channel += 1
) {
const sample = Math.max(
-1,
Math.min(1, audioBuffer.getChannelData(channel)[i])
);
samples[offset] = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
offset += 1;
}
}
return encodeToMP3(
audioBuffer.numberOfChannels,
audioBuffer.sampleRate,
samples,
bitrate
);
return encodeToMP3(wavHeader.channels, wavHeader.sampleRate, samples);
} catch (error) {
// eslint-disable-next-line
console.log('Failed to convert WAV to MP3:', error);
throw new Error('Conversion from WAV to MP3 failed.');
throw new Error('Conversion to MP3 failed.');
}
};
export const convertAudio = async (inputBlob, outputFormat, bitrate = 128) => {
let audio;
if (outputFormat === 'audio/wav') {
audio = await convertToWav(inputBlob);
} else if (outputFormat === 'audio/mp3') {
audio = await convertToMp3(inputBlob, bitrate);
} else {
throw new Error('Unsupported output format');
}
return audio;
};

View File

@@ -1,66 +0,0 @@
import { Bar } from 'vue-chartjs';
const fontFamily =
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
const defaultChartOptions = {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false,
labels: {
fontFamily,
},
},
animation: {
duration: 0,
},
datasets: {
bar: {
barPercentage: 1.0,
},
},
scales: {
xAxes: [
{
ticks: {
fontFamily,
},
gridLines: {
drawOnChartArea: false,
},
},
],
yAxes: [
{
ticks: {
fontFamily,
beginAtZero: true,
},
gridLines: {
drawOnChartArea: false,
},
},
],
},
};
export default {
extends: Bar,
props: {
collection: {
type: Object,
default: () => ({}),
},
chartOptions: {
type: Object,
default: () => ({}),
},
},
mounted() {
this.renderChart(this.collection, {
...defaultChartOptions,
...this.chartOptions,
});
},
};

View File

@@ -1,62 +0,0 @@
import { HorizontalBar } from 'vue-chartjs';
const chartOptions = {
responsive: true,
legend: {
display: false,
},
title: {
display: false,
},
tooltips: {
enabled: false,
},
scales: {
xAxes: [
{
gridLines: {
offsetGridLines: false,
},
display: false,
stacked: true,
},
],
yAxes: [
{
gridLines: {
offsetGridLines: false,
},
display: false,
stacked: true,
},
],
},
};
export default {
extends: HorizontalBar,
props: {
collection: {
type: Object,
default: () => {},
},
chartOptions: {
type: Object,
default: () => {},
},
},
watch: {
collection() {
this.renderChart(this.collection, {
...chartOptions,
...this.chartOptions,
});
},
},
mounted() {
this.renderChart(this.collection, {
...chartOptions,
...this.chartOptions,
});
},
};

View File

@@ -35,7 +35,7 @@ export default {
this.$store.dispatch('getCannedResponse', { searchKey: this.searchKey });
},
handleMentionClick(item = {}) {
this.$emit('click', item.description);
this.$emit('replace', item.description);
},
},
};

View File

@@ -9,6 +9,7 @@ import { useFilter } from 'shared/composables/useFilter';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
import { useTrack } from 'dashboard/composables';
export default {
components: {
@@ -141,8 +142,27 @@ export default {
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type?.filterOperators;
},
statusFilterItems() {
return {
open: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.open.TEXT'),
},
resolved: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.resolved.TEXT'),
},
pending: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.pending.TEXT'),
},
snoozed: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.snoozed.TEXT'),
},
all: {
TEXT: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.all.TEXT'),
},
};
},
getDropdownValues(type) {
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
const statusFilters = this.statusFilterItems();
const allCustomAttributes = this.$store.getters[
'attributes/getAttributesByModel'
](this.attributeModel);
@@ -268,7 +288,7 @@ export default {
JSON.parse(JSON.stringify(this.appliedFilters))
);
this.$emit('applyFilter', this.appliedFilters);
this.$track(CONVERSATION_EVENTS.APPLY_FILTER, {
useTrack(CONVERSATION_EVENTS.APPLY_FILTER, {
applied_filters: this.appliedFilters.map(filter => ({
key: filter.attribute_key,
operator: filter.filter_operator,

View File

@@ -4,6 +4,25 @@ import { mapGetters } from 'vuex';
import FilterItem from './FilterItem.vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
const CHAT_STATUS_FILTER_ITEMS = Object.freeze([
'open',
'resolved',
'pending',
'snoozed',
'all',
]);
const SORT_ORDER_ITEMS = Object.freeze([
'last_activity_at_asc',
'last_activity_at_desc',
'created_at_desc',
'created_at_asc',
'priority_desc',
'priority_asc',
'waiting_since_asc',
'waiting_since_desc',
]);
export default {
components: {
FilterItem,
@@ -18,8 +37,8 @@ export default {
data() {
return {
showActionsDropdown: false,
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
chatSortItems: this.$t('CHAT_LIST.SORT_ORDER_ITEMS'),
chatStatusItems: CHAT_STATUS_FILTER_ITEMS,
chatSortItems: SORT_ORDER_ITEMS,
};
},
computed: {

View File

@@ -46,10 +46,12 @@ export default {
return [
{
key: 'messages',
index: 0,
name: this.$t('CONVERSATION.DASHBOARD_APP_TAB_MESSAGES'),
},
...this.dashboardApps.map(dashboardApp => ({
...this.dashboardApps.map((dashboardApp, index) => ({
key: `dashboard-${dashboardApp.id}`,
index: index + 1,
name: dashboardApp.title,
})),
];
@@ -112,6 +114,7 @@ export default {
<woot-tabs-item
v-for="tab in dashboardAppTabs"
:key="tab.key"
:index="tab.index"
:name="tab.name"
:show-badge="false"
/>

View File

@@ -12,6 +12,7 @@ import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
import SLACardLabel from './components/SLACardLabel.vue';
import ContextMenu from 'dashboard/components/ui/ContextMenu.vue';
export default {
components: {
@@ -23,6 +24,7 @@ export default {
MessagePreview,
PriorityMark,
SLACardLabel,
ContextMenu,
},
mixins: [inboxMixin],
props: {
@@ -316,17 +318,13 @@ export default {
{{ unreadCount > 9 ? '9+' : unreadCount }}
</span>
</div>
<CardLabels
:conversation-id="chat.id"
:conversation-labels="chat.labels"
class="mt-0.5 mx-2 mb-0"
>
<CardLabels :conversation-labels="chat.labels" class="mt-0.5 mx-2 mb-0">
<template v-if="hasSlaPolicyId" #before>
<SLACardLabel :chat="chat" class="ltr:mr-1 rtl:ml-1" />
</template>
</CardLabels>
</div>
<woot-context-menu
<ContextMenu
v-if="showContextMenu"
:x="contextMenu.x"
:y="contextMenu.y"
@@ -345,7 +343,7 @@ export default {
@markAsUnread="markAsUnread"
@assignPriority="assignPriority"
/>
</woot-context-menu>
</ContextMenu>
</div>
</template>

View File

@@ -132,7 +132,7 @@ export default {
<div v-if="sentToOtherEmailAddress" class="w-[50%] mt-1">
<label :class="{ error: v$.email.$error }">
<input
v-model.trim="email"
v-model="email"
type="text"
:placeholder="$t('EMAIL_TRANSCRIPT.FORM.EMAIL.PLACEHOLDER')"
@input="v$.email.$touch"

View File

@@ -56,7 +56,7 @@ export default {
!this.loadingChatList &&
this.isAdmin
) {
return 'h-full overflow-auto';
return 'h-full overflow-auto w-full';
}
return 'flex-1 min-w-0 px-0 flex flex-col items-center justify-center h-full';
},
@@ -73,7 +73,7 @@ export default {
<!-- No inboxes attached -->
<div
v-if="!inboxesList.length && !uiFlags.isFetching && !loadingChatList"
class="clearfix"
class="clearfix mx-auto"
>
<OnboardingView v-if="isAdmin" />
<EmptyStateMessage v-else :message="$t('CONVERSATION.NO_INBOX_AGENT')" />

View File

@@ -15,12 +15,12 @@ export default {
<div class="flex flex-col items-center justify-center h-full">
<img
class="m-4 w-32 hidden dark:block"
src="~dashboard/assets/images/no-chat-dark.svg"
src="dashboard/assets/images/no-chat-dark.svg"
alt="No Chat dark"
/>
<img
class="m-4 w-32 block dark:hidden"
src="~dashboard/assets/images/no-chat.svg"
src="dashboard/assets/images/no-chat.svg"
alt="No Chat"
/>
<span

View File

@@ -6,7 +6,7 @@ export default {
required: true,
},
items: {
type: Object,
type: Array,
required: true,
},
type: {
@@ -42,8 +42,8 @@ export default {
class="bg-slate-25 dark:bg-slate-700 text-xs h-6 my-0 mx-1 py-0 pr-6 pl-2 w-32 border border-solid border-slate-75 dark:border-slate-600 text-slate-800 dark:text-slate-100"
@change="onTabChange()"
>
<option v-for="(value, status) in items" :key="status" :value="status">
{{ $t(`${pathPrefix}.${status}.TEXT`) }}
<option v-for="value in items" :key="value" :value="value">
{{ $t(`${pathPrefix}.${value}.TEXT`) }}
</option>
</select>
</template>

View File

@@ -21,7 +21,9 @@ import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
import * as Sentry from '@sentry/browser';
import * as Sentry from '@sentry/vue';
import { useTrack } from 'dashboard/composables';
import { emitter } from 'shared/helpers/mitt';
export default {
components: {
@@ -349,11 +351,11 @@ export default {
},
mounted() {
this.hasMediaLoadError = false;
this.$emitter.on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
emitter.on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer();
},
beforeDestroy() {
this.$emitter.off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
unmounted() {
emitter.off(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
clearTimeout(this.higlightTimeout);
},
methods: {
@@ -366,9 +368,6 @@ export default {
if (this.hasAttachments && this.data.attachments.length > 0) {
return this.compareMessageFileType(this.data, type);
}
if (this.storyReply) {
return true;
}
return false;
},
compareMessageFileType(messageData, type) {
@@ -406,7 +405,7 @@ export default {
e.preventDefault();
if (e.type === 'contextmenu') {
this.$track(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
useTrack(ACCOUNT_EVENTS.OPEN_MESSAGE_CONTEXT_MENU);
}
this.contextMenuPosition = {
x: e.pageX || e.clientX,
@@ -423,7 +422,7 @@ export default {
const { conversation_id: conversationId, id: replyTo } = this.data;
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
},
setupHighlightTimer() {
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {

View File

@@ -18,6 +18,7 @@ import { mapGetters } from 'vuex';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
// utils
import { emitter } from 'shared/helpers/mitt';
import { getTypingUsersText } from '../../../helper/commons';
import { calculateScrollTop } from './helpers/scrollTopCalculationHelper';
import { LocalStorage } from 'shared/helpers/localStorage';
@@ -251,12 +252,12 @@ export default {
},
created() {
this.$emitter.on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
emitter.on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
// when a new message comes in, we refetch the label suggestions
this.$emitter.on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
emitter.on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
// when a message is sent we set the flag to true this hides the label suggestions,
// until the chat is changed and the flag is reset in the watch for currentChat
this.$emitter.on(BUS_EVENTS.MESSAGE_SENT, () => {
emitter.on(BUS_EVENTS.MESSAGE_SENT, () => {
this.messageSentSinceOpened = true;
});
},
@@ -267,7 +268,7 @@ export default {
this.fetchSuggestions();
},
beforeDestroy() {
unmounted() {
this.removeBusListeners();
this.removeScrollListener();
},
@@ -323,7 +324,7 @@ export default {
this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
},
removeBusListeners() {
this.$emitter.off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
emitter.off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
},
onScrollToMessage({ messageId = '' } = {}) {
this.$nextTick(() => {
@@ -428,7 +429,7 @@ export default {
} else {
this.hasUserScrolled = true;
}
this.$emitter.emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
emitter.emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL);
this.fetchPreviousMessages(e.target.scrollTop);
},
@@ -540,7 +541,7 @@ export default {
{{ typingUserNames }}
<img
class="w-6 ltr:ml-2 rtl:mr-2"
src="~dashboard/assets/images/typing.gif"
src="assets/images/typing.gif"
alt="Someone is typing"
/>
</div>
@@ -548,7 +549,7 @@ export default {
<ReplyBox
:conversation-id="currentChat.id"
:popout-reply-box.sync="isPopOutReplyBox"
@click="showPopOutReplyBox"
@togglePopout="showPopOutReplyBox"
/>
</div>
</div>

View File

@@ -1,6 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { emitter } from 'shared/helpers/mitt';
import EmailTranscriptModal from './EmailTranscriptModal.vue';
import ResolveAction from '../../buttons/ResolveAction.vue';
import {
@@ -23,14 +24,14 @@ export default {
...mapGetters({ currentChat: 'getSelectedChat' }),
},
mounted() {
this.$emitter.on(CMD_MUTE_CONVERSATION, this.mute);
this.$emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute);
this.$emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
emitter.on(CMD_MUTE_CONVERSATION, this.mute);
emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute);
emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
},
destroyed() {
this.$emitter.off(CMD_MUTE_CONVERSATION, this.mute);
this.$emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute);
this.$emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
emitter.off(CMD_MUTE_CONVERSATION, this.mute);
emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute);
emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
},
methods: {
mute() {

View File

@@ -31,7 +31,9 @@ defineProps({
<div
class="h-full w-full bg-white dark:bg-slate-900 border border-slate-100 dark:border-white/10 rounded-lg p-4 flex flex-col"
>
<img :src="imageSrc" :alt="imageAlt" class="h-36 w-auto mx-auto" />
<div class="flex-1 flex items-center justify-center">
<img :src="imageSrc" :alt="imageAlt" class="h-36 w-auto mx-auto" />
</div>
<div class="mt-auto">
<p
class="text-base text-slate-800 dark:text-slate-100 font-interDisplay font-semibold tracking-[0.3px]"
@@ -47,7 +49,7 @@ defineProps({
class="no-underline text-woot-500 text-sm font-medium"
>
<span>{{ linkText }}</span>
<span>{{ `` }}</span>
<span class="ml-2">{{ `` }}</span>
</router-link>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<script setup>
import OnboardingFeatureCard from './OnboardingFeatureCard.vue';
import { computed } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { useStoreGetters } from 'dashboard/composables/store';
const getters = useStoreGetters();
@@ -28,7 +28,7 @@ const greetingMessage = computed(() => {
<template>
<div
class="min-h-screen max-w-4xl mx-auto grid grid-cols-1 xl:grid-cols-2 grid-rows-[auto_1fr_1fr] gap-4 p-8 w-full font-inter overflow-auto"
class="min-h-screen lg:max-w-5xl max-w-4xl mx-auto grid grid-cols-2 grid-rows-[auto_1fr_1fr] auto-rows-min gap-4 p-8 w-full font-inter overflow-auto"
>
<div class="col-span-full self-start">
<p

View File

@@ -1,7 +1,9 @@
<script>
import { defineAsyncComponent } from 'vue';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useTrack } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import CannedResponse from './CannedResponse.vue';
@@ -12,7 +14,7 @@ import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel
import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
@@ -40,8 +42,10 @@ import {
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
import { emitter } from 'shared/helpers/mitt';
const EmojiInput = defineAsyncComponent(
() => import('shared/components/emoji/EmojiInput.vue')
);
export default {
components: {
@@ -449,15 +453,12 @@ export default {
);
this.fetchAndSetReplyTo();
this.$emitter.on(
BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE,
this.fetchAndSetReplyTo
);
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
// A hacky fix to solve the drag and drop
// Is showing on top of new conversation modal drag and drop
// TODO need to find a better solution
this.$emitter.on(
emitter.on(
BUS_EVENTS.NEW_CONVERSATION_MODAL,
this.onNewConversationModalActive
);
@@ -465,13 +466,10 @@ export default {
destroyed() {
document.removeEventListener('paste', this.onPaste);
document.removeEventListener('keydown', this.handleKeyEvents);
this.$emitter.off(
BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE,
this.fetchAndSetReplyTo
);
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
},
beforeDestroy() {
this.$emitter.off(
unmounted() {
emitter.off(
BUS_EVENTS.NEW_CONVERSATION_MODAL,
this.onNewConversationModalActive
);
@@ -484,7 +482,7 @@ export default {
const lines = title.split('\n');
const nonEmptyLines = lines.filter(line => line.trim() !== '');
const filteredMarkdown = nonEmptyLines.join(' ');
this.$emitter.emit(
emitter.emit(
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
`[${filteredMarkdown}](${url})`
);
@@ -494,7 +492,7 @@ export default {
);
}
this.$track(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
useTrack(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
},
toggleRichContentEditor() {
this.updateUISettings({
@@ -689,8 +687,8 @@ export default {
sendMessageAnalyticsData(isPrivate) {
// Analytics data for message signature is enabled or not in channels
return isPrivate
? this.$track(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
? useTrack(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
: useTrack(CONVERSATION_EVENTS.SENT_MESSAGE, {
channelType: this.channelType,
signatureEnabled: this.sendWithSignature,
hasReplyTo: !!this.inReplyTo?.id,
@@ -726,8 +724,8 @@ export default {
'createPendingMessageAndSend',
messagePayload
);
this.$emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
this.$emitter.emit(BUS_EVENTS.MESSAGE_SENT);
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
emitter.emit(BUS_EVENTS.MESSAGE_SENT);
this.removeFromDraft();
this.sendMessageAnalyticsData(messagePayload.private);
} catch (error) {
@@ -757,7 +755,7 @@ export default {
});
setTimeout(() => {
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
useTrack(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.message = updatedMessage;
}, 100);
},
@@ -832,7 +830,7 @@ export default {
if (this.isRecordingAudio) {
if (!this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopAudioRecording();
this.$refs.audioRecorderInput.stopRecording();
} else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
}
@@ -859,7 +857,7 @@ export default {
onFocus() {
this.isFocused = true;
},
onStateProgressRecorderChanged(duration) {
onRecordProgressChanged(duration) {
this.recordingAudioDurationText = duration;
},
onStateRecorderChanged(state) {
@@ -1053,7 +1051,7 @@ export default {
resetReplyToMessage() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
},
onNewConversationModalActive(isActive) {
// Issue is if the new conversation modal is open and we drag and drop the file
@@ -1083,7 +1081,7 @@ export default {
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
has-action-button
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
@click="onClickSelfAssign"
@primaryAction="onClickSelfAssign"
/>
<ReplyTopPanel
:mode="replyType"
@@ -1091,7 +1089,7 @@ export default {
:characters-remaining="charactersRemaining"
:popout-reply-box="popoutReplyBox"
@setReplyMode="setReplyMode"
@click="$emit('click')"
@togglePopout="$emit('togglePopout')"
/>
<ArticleSearchPopover
v-if="showArticleSearchPopover && connectedPortalSlug"
@@ -1110,7 +1108,7 @@ export default {
v-on-clickaway="hideMentions"
class="normal-editor__canned-box"
:search-key="mentionSearchKey"
@click="replaceText"
@replace="replaceText"
/>
<EmojiInput
v-if="showEmojiPicker"
@@ -1120,15 +1118,15 @@ export default {
/>
<ReplyEmailHead
v-if="showReplyHead"
:cc-emails.sync="ccEmails"
:bcc-emails.sync="bccEmails"
:to-emails.sync="toEmails"
v-model:cc-emails="ccEmails"
v-model:bcc-emails="bccEmails"
v-model:to-emails="toEmails"
/>
<WootAudioRecorder
v-if="showAudioRecorderEditor"
ref="audioRecorderInput"
:audio-record-format="audioRecordFormat"
@stateRecorderProgressChanged="onStateProgressRecorderChanged"
@recorderProgressChanged="onRecordProgressChanged"
@stateRecorderChanged="onStateRecorderChanged"
@finishRecord="onFinishRecorder"
/>

View File

@@ -90,7 +90,7 @@ export default {
</label>
<div class="rounded-none flex-1 min-w-0 m-0 whitespace-nowrap">
<woot-input
v-model.trim="v$.toEmailsVal.$model"
v-model="v$.toEmailsVal.$model"
type="text"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none"
:class="{ error: v$.toEmailsVal.$error }"
@@ -107,7 +107,7 @@ export default {
</label>
<div class="rounded-none flex-1 min-w-0 m-0 whitespace-nowrap">
<woot-input
v-model.trim="v$.ccEmailsVal.$model"
v-model="v$.ccEmailsVal.$model"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none"
type="text"
:class="{ error: v$.ccEmailsVal.$error }"
@@ -135,7 +135,7 @@ export default {
</label>
<div class="rounded-none flex-1 min-w-0 m-0 whitespace-nowrap">
<woot-input
v-model.trim="v$.bccEmailsVal.$model"
v-model="v$.bccEmailsVal.$model"
type="text"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none"
:class="{ error: v$.bccEmailsVal.$error }"

View File

@@ -10,7 +10,7 @@ const props = defineProps({
},
});
const emit = defineEmits(['click']);
const emit = defineEmits(['selectAgent']);
const getters = useStoreGetters();
const agents = computed(() => getters['agents/getVerifiedAgents'].value);
@@ -36,7 +36,7 @@ const adjustScroll = () => {
};
const onSelect = () => {
emit('click', items.value[selectedIndex.value]);
emit('selectAgent', items.value[selectedIndex.value]);
};
useKeyboardNavigableList({

View File

@@ -50,7 +50,7 @@ export default {
},
methods: {
handleVariableClick(item = {}) {
this.$emit('click', item.key);
this.$emit('selectVariable', item.key);
},
},
};

View File

@@ -179,9 +179,11 @@ footer {
@apply ml-2.5;
}
}
.error {
@apply bg-red-100 dark:bg-red-100 rounded-md text-red-800 dark:text-red-800 p-2.5 text-center;
}
.template-input {
@apply bg-slate-25 dark:bg-slate-900 text-slate-700 dark:text-slate-100;
}

View File

@@ -46,7 +46,7 @@ export default {
</template>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables';
@import 'dashboard/assets/scss/variables';
.file {
display: flex;

View File

@@ -2,6 +2,7 @@
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
import { MESSAGE_TYPE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { emitter } from 'shared/helpers/mitt';
export default {
name: 'ReplyTo',
@@ -27,7 +28,7 @@ export default {
},
methods: {
scrollToMessage() {
this.$emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
messageId: this.message.id,
});
},

View File

@@ -74,7 +74,7 @@ export default {
this.updateSlaStatus();
this.createTimer();
},
beforeDestroy() {
unmounted() {
if (this.timer) {
clearTimeout(this.timer);
}

View File

@@ -8,6 +8,7 @@ import MenuItem from './menuItem.vue';
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
import wootConstants from 'dashboard/constants/globals';
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
export default {
components: {
MenuItem,
@@ -192,7 +193,7 @@ export default {
v-if="!hasUnreadMessages"
:option="unreadOption"
variant="icon"
@click="$emit('markAsUnread')"
@click.stop="$emit('markAsUnread')"
/>
<template v-for="option in statusMenuConfig">
<MenuItem
@@ -200,14 +201,14 @@ export default {
:key="option.key"
:option="option"
variant="icon"
@click="toggleStatus(option.key, null)"
@click.stop="toggleStatus(option.key, null)"
/>
</template>
<MenuItem
v-if="showSnooze"
:option="snoozeOption"
variant="icon"
@click="snoozeConversation()"
@click.stop="snoozeConversation()"
/>
<MenuItemWithSubmenu :option="priorityConfig">
@@ -215,7 +216,7 @@ export default {
v-for="(option, i) in priorityConfig.options"
:key="i"
:option="option"
@click="assignPriority(option.key)"
@click.stop="assignPriority(option.key)"
/>
</MenuItemWithSubmenu>
<MenuItemWithSubmenu
@@ -227,7 +228,7 @@ export default {
:key="label.id"
:option="generateMenuLabelConfig(label, 'label')"
variant="label"
@click="$emit('assignLabel', label)"
@click.stop="$emit('assignLabel', label)"
/>
</MenuItemWithSubmenu>
<MenuItemWithSubmenu
@@ -241,7 +242,7 @@ export default {
:key="agent.id"
:option="generateMenuLabelConfig(agent, 'agent')"
variant="agent"
@click="$emit('assignAgent', agent)"
@click.stop="$emit('assignAgent', agent)"
/>
</template>
</MenuItemWithSubmenu>
@@ -253,7 +254,7 @@ export default {
v-for="team in teams"
:key="team.id"
:option="generateMenuLabelConfig(team, 'team')"
@click="$emit('assignTeam', team)"
@click.stop="$emit('assignTeam', team)"
/>
</MenuItemWithSubmenu>
</div>

View File

@@ -18,11 +18,7 @@ export default {
</script>
<template>
<div
class="menu text-slate-800 dark:text-slate-100"
role="button"
@click.stop="$emit('click')"
>
<div class="menu text-slate-800 dark:text-slate-100" role="button">
<fluent-icon
v-if="variant === 'icon' && option.icon"
:icon="option.icon"

View File

@@ -5,6 +5,7 @@ import Avatar from '../../Avatar.vue';
// composables
import { useAI } from 'dashboard/composables/useAI';
import { useTrack } from 'dashboard/composables';
// store & api
import { mapGetters } from 'vuex';
@@ -143,7 +144,7 @@ export default {
: this.suggestedLabels,
};
this.$track(event, payload);
useTrack(event, payload);
},
},
};

View File

@@ -1,6 +1,7 @@
<script>
import { getUnixTime } from 'date-fns';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import { emitter } from 'shared/helpers/mitt';
import wootConstants from 'dashboard/constants/globals';
import {
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
@@ -58,29 +59,29 @@ export default {
};
},
mounted() {
this.$emitter.on(
emitter.on(
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
this.onCmdSnoozeConversation
);
this.$emitter.on(
emitter.on(
CMD_BULK_ACTION_REOPEN_CONVERSATION,
this.onCmdReopenConversation
);
this.$emitter.on(
emitter.on(
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
this.onCmdResolveConversation
);
},
destroyed() {
this.$emitter.off(
emitter.off(
CMD_BULK_ACTION_SNOOZE_CONVERSATION,
this.onCmdSnoozeConversation
);
this.$emitter.off(
emitter.off(
CMD_BULK_ACTION_REOPEN_CONVERSATION,
this.onCmdReopenConversation
);
this.$emitter.off(
emitter.off(
CMD_BULK_ACTION_RESOLVE_CONVERSATION,
this.onCmdResolveConversation
);

View File

@@ -1,5 +1,5 @@
<script setup>
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';

View File

@@ -1,82 +1,67 @@
<script>
import { computed } from 'vue';
<script setup>
import { ref, computed, watch, onMounted, nextTick, useSlots } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
export default {
props: {
conversationLabels: {
type: Array,
required: true,
},
const props = defineProps({
conversationLabels: {
type: Array,
required: true,
},
setup(props) {
const accountLabels = useMapGetter('labels/getLabels');
});
const activeLabels = computed(() => {
return accountLabels.value.filter(({ title }) =>
props.conversationLabels.includes(title)
);
});
const slots = useSlots();
const accountLabels = useMapGetter('labels/getLabels');
return {
activeLabels,
};
},
data() {
return {
showAllLabels: false,
showExpandLabelButton: false,
labelPosition: -1,
};
},
watch: {
activeLabels() {
this.$nextTick(() => this.computeVisibleLabelPosition());
},
},
mounted() {
// the problem here is that there is a certain amount of delay between the conversation
// card being mounted and the resize event eventually being triggered
// This means we need to run the function immediately after the component is mounted
// Happens especially when used in a virtual list.
// We can make the first trigger, a standard part of the directive, in case
// we face this issue again
this.computeVisibleLabelPosition();
},
methods: {
onShowLabels(e) {
e.stopPropagation();
this.showAllLabels = !this.showAllLabels;
this.$nextTick(() => this.computeVisibleLabelPosition());
},
computeVisibleLabelPosition() {
const beforeSlot = this.$slots.before ? 100 : 0;
const labelContainer = this.$refs.labelContainer;
if (!labelContainer) return;
const activeLabels = computed(() => {
return accountLabels.value.filter(({ title }) =>
props.conversationLabels.includes(title)
);
});
const labels = Array.from(labelContainer.querySelectorAll('.label'));
let labelOffset = 0;
this.showExpandLabelButton = false;
labels.forEach((label, index) => {
labelOffset += label.offsetWidth + 8;
if (labelOffset < labelContainer.clientWidth - 16 - beforeSlot) {
this.labelPosition = index;
} else {
this.showExpandLabelButton = labels.length > 1;
}
});
},
},
const showAllLabels = ref(false);
const showExpandLabelButton = ref(false);
const labelPosition = ref(-1);
const labelContainer = ref(null);
const computeVisibleLabelPosition = () => {
const beforeSlot = slots.before ? 100 : 0;
if (!labelContainer.value) {
return;
}
const labels = Array.from(labelContainer.value.querySelectorAll('.label'));
let labelOffset = 0;
showExpandLabelButton.value = false;
labels.forEach((label, index) => {
labelOffset += label.offsetWidth + 8;
if (labelOffset < labelContainer.value.clientWidth - beforeSlot) {
labelPosition.value = index;
} else {
showExpandLabelButton.value = labels.length > 1;
}
});
};
watch(activeLabels, () => {
nextTick(() => computeVisibleLabelPosition());
});
onMounted(() => {
computeVisibleLabelPosition();
});
const onShowLabels = e => {
e.stopPropagation();
showAllLabels.value = !showAllLabels.value;
nextTick(() => computeVisibleLabelPosition());
};
</script>
<template>
<div
v-if="activeLabels.length || $slots.before"
ref="labelContainer"
v-resize="computeVisibleLabelPosition"
>
<div ref="labelContainer" v-resize="computeVisibleLabelPosition">
<div
v-if="activeLabels.length || $slots.before"
class="flex items-end flex-shrink min-w-0 gap-y-1"
:class="{ 'h-auto overflow-visible flex-row flex-wrap': showAllLabels }"
>
@@ -90,7 +75,9 @@ export default {
variant="smooth"
class="!mb-0 max-w-[calc(100%-0.5rem)]"
small
:class="{ hidden: !showAllLabels && index > labelPosition }"
:class="{
'invisible absolute': !showAllLabels && index > labelPosition,
}"
/>
<woot-button
v-if="showExpandLabelButton"
@@ -123,8 +110,4 @@ export default {
@apply border border-solid border-slate-100 dark:border-slate-700;
}
}
.hidden {
@apply invisible absolute;
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup>
import { reactive, computed, onMounted, ref } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import validations from './validations';

View File

@@ -1,5 +1,5 @@
<script setup>
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import LinkIssue from './LinkIssue.vue';
import CreateIssue from './CreateIssue.vue';
@@ -65,8 +65,9 @@ const onClickTabChange = index => {
@change="onClickTabChange"
>
<woot-tabs-item
v-for="tab in tabs"
v-for="(tab, index) in tabs"
:key="tab.key"
:index="index"
:name="tab.name"
:show-badge="false"
/>

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
@@ -121,7 +121,7 @@ const linkIssue = async () => {
enable-search
class="left-0 flex flex-col w-full overflow-y-auto h-fit !max-h-[160px] md:left-auto md:right-0 top-10"
@onSearch="onSearch"
@click="onSelectIssue"
@select="onSelectIssue"
/>
</template>
</FilterButton>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, computed, defineComponent } from 'vue';
import { ref, computed, defineOptions } from 'vue';
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
@@ -14,7 +14,7 @@ const props = defineProps({
const emit = defineEmits(['change']);
defineComponent({
defineOptions({
name: 'SearchableDropdown',
});
@@ -64,7 +64,7 @@ const selectedItemId = computed(() => selectedItem.value?.id || null);
:input-placeholder="placeholder"
enable-search
class="left-0 flex flex-col w-full overflow-y-auto h-fit !max-h-[160px] md:left-auto md:right-0 top-10"
@click="onSelect"
@select="onSelect"
/>
</template>
</FilterButton>

View File

@@ -1,8 +1,8 @@
<script setup>
import { computed, ref, onMounted, watch, defineComponent, provide } from 'vue';
import { computed, ref, onMounted, watch, defineOptions, provide } from 'vue';
import { useAlert } from 'dashboard/composables';
import { useStoreGetters } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import LinearAPI from 'dashboard/api/integrations/linear';
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
import Issue from './Issue.vue';
@@ -15,7 +15,7 @@ const props = defineProps({
},
});
defineComponent({
defineOptions({
name: 'Linear',
});

View File

@@ -1,7 +1,7 @@
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip';
import FloatingVue from 'floating-vue';
import Button from 'dashboard/components/buttons/Button.vue';
import i18n from 'dashboard/i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
@@ -20,7 +20,7 @@ import { emitter } from 'shared/helpers/mitt';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.use(VTooltip);
localVue.use(FloatingVue);
localVue.component('fluent-icon', FluentIcon);
localVue.component('woot-button', Button);

View File

@@ -1,28 +0,0 @@
import ContactBubble from '../bubble/Contact.vue';
export default {
title: 'Components/Messaging/ContactBubble',
component: ContactBubble,
argTypes: {
name: {
defaultValue: 'Eden Hazard',
control: {
type: 'string',
},
},
phoneNumber: {
defaultValue: '+517554433220',
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ContactBubble },
template: '<contact-bubble v-bind="$props" />',
});
export const ContactBubbleView = Template.bind({});

View File

@@ -1,34 +0,0 @@
import LocationBubble from '../bubble/Location.vue';
export default {
title: 'Components/Help Center',
component: LocationBubble,
argTypes: {
latitude: {
defaultValue: 1,
control: {
type: 'number',
},
},
longitude: {
defaultValue: 1,
control: {
type: 'number',
},
},
name: {
defaultValue: '420, Dope street',
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { LocationBubble },
template: '<location-bubble v-bind="$props" />',
});
export const LocationBubbleView = Template.bind({});

View File

@@ -1,27 +0,0 @@
import ReplyEmailHead from '../ReplyEmailHead';
export default {
title: 'Components/ReplyBox/EmailHead',
component: ReplyEmailHead,
argTypes: {
ccEmails: {
control: {
type: 'string',
},
},
bccEmails: {
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ReplyEmailHead },
template:
'<reply-email-head v-bind="$props" @add="onAdd" @click="onClick"></reply-email-head>',
});
export const Add = Template.bind({});

View File

@@ -23,7 +23,7 @@ export default {
handleImageUpload(event) {
const [file] = event.target.files;
this.$emit('change', {
this.$emit('onAvatarSelect', {
file,
url: file ? URL.createObjectURL(file) : null,
});

View File

@@ -1,11 +1,15 @@
<script>
/**
* @deprecated This component is deprecated and will be removed in the next major version.
* Please use v3/components/Form/Input.vue instead
*/
export default {
props: {
label: {
type: String,
default: '',
},
value: {
modelValue: {
type: [String, Number],
default: '',
},
@@ -34,9 +38,16 @@ export default {
default: () => {},
},
},
created() {
// eslint-disable-next-line
console.warn(
'[DEPRECATED] <WootInput> has be deprecated and will be removed soon. Please use v3/components/Form/Input.vue instead'
);
},
methods: {
onChange(e) {
this.$emit('input', e.target.value);
this.$emit('update:modelValue', e.target.value);
},
onBlur(e) {
this.$emit('blur', e.target.value);
@@ -50,7 +61,7 @@ export default {
<span v-if="label">{{ label }}</span>
<input
class="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600"
:value="value"
:value="modelValue"
:type="type"
:placeholder="placeholder"
:readonly="readonly"

View File

@@ -1,12 +1,12 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { useVuelidate } from '@vuelidate/core';
import Modal from '../../Modal.vue';
export default {
components: {
Modal,
},
props: {
show: {
type: Boolean,

View File

@@ -1,17 +0,0 @@
import WootKeyboardShortcutModal from './WootKeyShortcutModal.vue';
export default {
title: 'Components/Shortcuts/Keyboard Shortcut',
component: WootKeyboardShortcutModal,
argTypes: {},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { WootKeyboardShortcutModal },
template:
'<woot-keyboard-shortcut-modal v-bind="$props"></woot-keyboard-shortcut-modal>',
});
export const KeyboardShortcut = Template.bind({});
KeyboardShortcut.args = {};

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { useDetectKeyboardLayout } from 'dashboard/composables/useDetectKeyboardLayout';
import { SHORTCUT_KEYS } from './constants';
import {

View File

@@ -1,69 +0,0 @@
import ThumbnailGroup from '../ThumbnailGroup.vue';
export default {
title: 'Components/ThumbnailGroup',
component: ThumbnailGroup,
argTypes: {
usersList: {
defaultValue: [
{
name: 'John',
id: 1,
thumbnail: '',
},
{
name: 'John',
id: 2,
thumbnail: '',
},
{
name: 'John',
id: 3,
thumbnail: '',
},
{
name: 'John',
id: 4,
thumbnail: '',
},
{
name: 'John',
id: 5,
thumbnail: '',
},
{
name: 'John',
id: 6,
thumbnail: '',
},
],
control: {
type: 'object',
},
},
size: {
control: {
type: 'text',
},
},
moreThumbnailsText: {
control: {
type: 'text',
default: '2 more',
},
},
showMoreThumbnailsCount: {
control: {
type: 'boolean',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ThumbnailGroup },
template: '<ThumbnailGroup v-bind="$props"/>',
});
export const Primary = Template.bind({});