Merge branch 'develop' into fix/cw-4085
This commit is contained in:
@@ -353,10 +353,11 @@ function setFiltersFromUISettings() {
|
||||
const { conversations_filter_by: filterBy = {} } = uiSettings.value;
|
||||
const { status, order_by: orderBy } = filterBy;
|
||||
activeStatus.value = status || wootConstants.STATUS_TYPE.OPEN;
|
||||
activeSortBy.value =
|
||||
Object.keys(wootConstants.SORT_BY_TYPE).find(
|
||||
sortField => sortField === orderBy
|
||||
) || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
|
||||
activeSortBy.value = Object.values(wootConstants.SORT_BY_TYPE).includes(
|
||||
orderBy
|
||||
)
|
||||
? orderBy
|
||||
: wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
|
||||
}
|
||||
|
||||
function emitConversationLoaded() {
|
||||
|
||||
@@ -23,33 +23,34 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ml-0 mr-0 flex py-8 w-full xl:w-3/4 flex-col xl:flex-row"
|
||||
class="ml-0 mr-0 py-8 w-full"
|
||||
:class="{
|
||||
'border-b border-solid border-slate-50 dark:border-slate-700/30':
|
||||
showBorder,
|
||||
'border-b border-solid border-n-weak/60 dark:border-n-weak': showBorder,
|
||||
}"
|
||||
>
|
||||
<div class="w-full xl:w-1/4 min-w-0 xl:max-w-[30%] pr-12">
|
||||
<p
|
||||
v-if="title"
|
||||
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
|
||||
>
|
||||
{{ title }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm mb-2 text-slate-700 dark:text-slate-300 leading-5 tracking-normal mt-2"
|
||||
>
|
||||
<slot v-if="subTitle" name="subTitle">
|
||||
{{ subTitle }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-if="note">
|
||||
<span class="font-semibold">{{ $t('INBOX_MGMT.NOTE') }}</span>
|
||||
{{ note }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full xl:w-1/2 min-w-0 xl:max-w-[50%]">
|
||||
<slot />
|
||||
<div class="grid grid-cols-1 lg:grid-cols-6 gap-6">
|
||||
<div class="col-span-2 xl:col-span-1">
|
||||
<p
|
||||
v-if="title"
|
||||
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
|
||||
>
|
||||
{{ title }}
|
||||
</p>
|
||||
<p
|
||||
class="text-sm mb-2 text-slate-700 dark:text-slate-300 leading-5 tracking-normal mt-2"
|
||||
>
|
||||
<slot v-if="subTitle" name="subTitle">
|
||||
{{ subTitle }}
|
||||
</slot>
|
||||
</p>
|
||||
<p v-if="note">
|
||||
<span class="font-semibold">{{ $t('INBOX_MGMT.NOTE') }}</span>
|
||||
{{ note }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-span-4 lg:col-span-4 2xl:col-span-3">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watchEffect } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
|
||||
import ConversationAPI from 'dashboard/api/inbox/conversation';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { ref } from 'vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
@@ -13,10 +16,44 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const messages = ref([]);
|
||||
|
||||
const store = useStore();
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const inboxAssistant = useMapGetter('getCopilotAssistant');
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const messages = ref([]);
|
||||
const isCaptainTyping = ref(false);
|
||||
const selectedAssistantId = ref(null);
|
||||
|
||||
const activeAssistant = computed(() => {
|
||||
const preferredId = uiSettings.value.preferred_captain_assistant_id;
|
||||
|
||||
// If the user has selected a specific assistant, it takes first preference for Copilot.
|
||||
if (preferredId) {
|
||||
const preferredAssistant = assistants.value.find(a => a.id === preferredId);
|
||||
// Return the preferred assistant if found, otherwise continue to next cases
|
||||
if (preferredAssistant) return preferredAssistant;
|
||||
}
|
||||
|
||||
// If the above is not available, the assistant connected to the inbox takes preference.
|
||||
if (inboxAssistant.value) {
|
||||
const inboxMatchedAssistant = assistants.value.find(
|
||||
a => a.id === inboxAssistant.value.id
|
||||
);
|
||||
if (inboxMatchedAssistant) return inboxMatchedAssistant;
|
||||
}
|
||||
// If neither of the above is available, the first assistant in the account takes preference.
|
||||
return assistants.value[0];
|
||||
});
|
||||
|
||||
const setAssistant = async assistant => {
|
||||
selectedAssistantId.value = assistant.id;
|
||||
await updateUISettings({
|
||||
preferred_captain_assistant_id: assistant.id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
messages.value = [];
|
||||
@@ -42,7 +79,7 @@ const sendMessage = async message => {
|
||||
}))
|
||||
.slice(0, -1),
|
||||
message,
|
||||
assistant_id: 16,
|
||||
assistant_id: selectedAssistantId.value,
|
||||
}
|
||||
);
|
||||
messages.value.push({
|
||||
@@ -57,6 +94,17 @@ const sendMessage = async message => {
|
||||
isCaptainTyping.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.conversationId) {
|
||||
store.dispatch('getInboxCaptainAssistantById', props.conversationId);
|
||||
selectedAssistantId.value = activeAssistant.value?.id;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -65,6 +113,9 @@ const sendMessage = async message => {
|
||||
:support-agent="currentUser"
|
||||
:is-captain-typing="isCaptainTyping"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
:assistants="assistants"
|
||||
:active-assistant="activeAssistant"
|
||||
@set-assistant="setAssistant"
|
||||
@send-message="sendMessage"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
@@ -23,17 +23,16 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid border-slate-75 dark:border-slate-600"
|
||||
class="flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid border-n-strong"
|
||||
:class="{
|
||||
'bg-woot-25 dark:bg-slate-700 border border-solid border-woot-300 dark:border-woot-400':
|
||||
'bg-woot-25 dark:bg-n-solid-2 border border-solid border-n-blue-border':
|
||||
active,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center px-2 w-full h-10 bg-slate-50 dark:bg-slate-900 rounded-t-[5px] border-b border-solid border-slate-50 dark:border-slate-600"
|
||||
class="flex justify-between items-center rounded-t-md px-2 w-full h-10 bg-slate-50 dark:bg-slate-900 border-b border-solid border-n-strong"
|
||||
:class="{
|
||||
'bg-woot-50 border-b border-solid border-woot-75 dark:border-woot-700':
|
||||
active,
|
||||
'bg-woot-50 border-b border-solid border-n-blue-border': active,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center p-1 text-sm font-medium">{{ heading }}</div>
|
||||
|
||||
@@ -38,20 +38,20 @@ export default {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 19px;
|
||||
height: 1.188rem;
|
||||
position: relative;
|
||||
transition-duration: 200ms;
|
||||
transition-property: background-color;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
width: 34px;
|
||||
width: 2.125rem;
|
||||
|
||||
&.active {
|
||||
background-color: var(--w-500);
|
||||
}
|
||||
|
||||
&.small {
|
||||
width: 22px;
|
||||
height: 14px;
|
||||
width: 1.375rem;
|
||||
height: 0.875rem;
|
||||
|
||||
span {
|
||||
height: var(--space-one);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, provide, onMounted, computed } from 'vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { ref, useTemplateRef, provide, computed, watch } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
|
||||
const props = defineProps({
|
||||
index: {
|
||||
@@ -19,6 +19,12 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const tabsContainer = useTemplateRef('tabsContainer');
|
||||
const tabsList = useTemplateRef('tabsList');
|
||||
|
||||
const { width: containerWidth } = useElementSize(tabsContainer);
|
||||
const { width: listWidth } = useElementSize(tabsList);
|
||||
|
||||
const hasScroll = ref(false);
|
||||
|
||||
const activeIndex = computed({
|
||||
@@ -34,20 +40,16 @@ provide('updateActiveIndex', index => {
|
||||
});
|
||||
|
||||
const computeScrollWidth = () => {
|
||||
// TODO: use useElementSize from vueuse
|
||||
const tabElement = document.querySelector('.tabs');
|
||||
if (tabElement) {
|
||||
hasScroll.value = tabElement.scrollWidth > tabElement.clientWidth;
|
||||
if (tabsContainer.value && tabsList.value) {
|
||||
hasScroll.value = tabsList.value.scrollWidth > tabsList.value.clientWidth;
|
||||
}
|
||||
};
|
||||
|
||||
const onScrollClick = direction => {
|
||||
// TODO: use useElementSize from vueuse
|
||||
const tabElement = document.querySelector('.tabs');
|
||||
if (tabElement) {
|
||||
let scrollPosition = tabElement.scrollLeft;
|
||||
if (tabsContainer.value && tabsList.value) {
|
||||
let scrollPosition = tabsList.value.scrollLeft;
|
||||
scrollPosition += direction === 'left' ? -100 : 100;
|
||||
tabElement.scrollTo({
|
||||
tabsList.value.scrollTo({
|
||||
top: 0,
|
||||
left: scrollPosition,
|
||||
behavior: 'smooth',
|
||||
@@ -55,14 +57,19 @@ const onScrollClick = direction => {
|
||||
}
|
||||
};
|
||||
|
||||
useEventListener(window, 'resize', computeScrollWidth);
|
||||
onMounted(() => {
|
||||
computeScrollWidth();
|
||||
});
|
||||
// Watch for changes in element sizes with immediate execution
|
||||
watch(
|
||||
[containerWidth, listWidth],
|
||||
() => {
|
||||
computeScrollWidth();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="tabsContainer"
|
||||
:class="{
|
||||
'tabs--container--with-border': border,
|
||||
'tabs--container--compact': isCompact,
|
||||
@@ -76,7 +83,7 @@ onMounted(() => {
|
||||
>
|
||||
<fluent-icon icon="chevron-left" :size="16" />
|
||||
</button>
|
||||
<ul :class="{ 'tabs--with-scroll': hasScroll }" class="tabs">
|
||||
<ul ref="tabsList" :class="{ 'tabs--with-scroll': hasScroll }" class="tabs">
|
||||
<slot />
|
||||
</ul>
|
||||
<button
|
||||
|
||||
@@ -72,26 +72,26 @@ export default {
|
||||
|
||||
&.active {
|
||||
h3 {
|
||||
@apply text-woot-500 dark:text-woot-500;
|
||||
@apply text-n-blue-text dark:text-n-blue-text;
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
@apply bg-n-brand dark:bg-n-brand;
|
||||
}
|
||||
}
|
||||
|
||||
&.over {
|
||||
&::after {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
@apply bg-n-brand dark:bg-n-brand;
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
@apply bg-n-brand dark:bg-n-brand;
|
||||
}
|
||||
|
||||
& + .item {
|
||||
&::before {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
@apply bg-n-brand dark:bg-n-brand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,10 +201,10 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter {
|
||||
@apply bg-slate-50 dark:bg-slate-800 p-2 border border-solid border-slate-75 dark:border-slate-600 rounded-md mb-2;
|
||||
@apply bg-n-slate-3 dark:bg-n-solid-3 p-2 border border-solid border-n-strong dark:border-n-strong rounded-md mb-2;
|
||||
|
||||
&.is-a-macro {
|
||||
@apply mb-0 bg-white dark:bg-slate-700 p-0 border-0 rounded-none;
|
||||
@apply mb-0 bg-n-slate-2 dark:bg-n-solid-3 p-0 border-0 rounded-none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
getInputErrorClass(errorMessage) {
|
||||
return errorMessage
|
||||
? 'bg-red-50 dark:bg-red-800/50 border-red-100 dark:border-red-700/50'
|
||||
: 'bg-slate-50 dark:bg-slate-800 border-slate-75 dark:border-slate-700/50';
|
||||
: 'bg-n-slate-3 dark:bg-n-solid-3 border-n-strong dark:border-n-strong';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pt-4 pb-0 px-8 border-b border-solid border-n-weak">
|
||||
<div class="pt-4 pb-0 px-8 border-b border-solid border-n-weak/60">
|
||||
<h2 class="text-2xl text-slate-800 dark:text-slate-100 mb-1 font-medium">
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
|
||||
@@ -127,7 +127,7 @@ export default {
|
||||
const uploadRef = ref(false);
|
||||
|
||||
const keyboardEvents = {
|
||||
'Alt+KeyA': {
|
||||
'$mod+Alt+KeyA': {
|
||||
action: () => {
|
||||
// TODO: This is really hacky, we need to replace the file picker component with
|
||||
// a custom one, where the logic and the component markup is isolated.
|
||||
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-between h-[52px] gap-2 ltr:pl-3 rtl:pr-3">
|
||||
<div class="flex justify-between h-[3.25rem] gap-2 ltr:pl-3 rtl:pr-3">
|
||||
<EditorModeToggle
|
||||
:mode="mode"
|
||||
class="mt-3"
|
||||
|
||||
@@ -411,7 +411,7 @@ export default {
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
@apply h-10 w-10 flex items-center justify-center rounded-full cursor-pointer mt-4;
|
||||
@apply flex items-center justify-center rounded-full cursor-pointer mt-4;
|
||||
|
||||
input[type='checkbox'] {
|
||||
@apply m-0 cursor-pointer;
|
||||
|
||||
@@ -53,7 +53,7 @@ const showCopilotTab = computed(() =>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 min-w-[320px] w-[320px] 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background"
|
||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-80 min-w-80 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background"
|
||||
>
|
||||
<div v-if="showCopilotTab" class="p-2">
|
||||
<TabBar
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
import { reactive, computed, onMounted, ref } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import validations from './validations';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import SearchableDropdown from './SearchableDropdown.vue';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -45,7 +47,7 @@ const statusDesiredOrder = [
|
||||
];
|
||||
|
||||
const isCreating = ref(false);
|
||||
const inputStyles = { borderRadius: '12px', fontSize: '14px' };
|
||||
const inputStyles = { borderRadius: '0.75rem', fontSize: '0.875rem' };
|
||||
|
||||
const formState = reactive({
|
||||
title: '',
|
||||
@@ -188,6 +190,7 @@ const createIssue = async () => {
|
||||
const { id: issueId } = response.data;
|
||||
await LinearAPI.link_issue(props.conversationId, issueId, props.title);
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS'));
|
||||
useTrack(LINEAR_EVENTS.CREATE_ISSUE);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
@@ -209,7 +212,7 @@ onMounted(getTeams);
|
||||
v-model="formState.title"
|
||||
:class="{ error: v$.title.$error }"
|
||||
class="w-full"
|
||||
:styles="{ ...inputStyles, padding: '6px 12px' }"
|
||||
:styles="{ ...inputStyles, padding: '0.375rem 0.75rem' }"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TITLE.LABEL')"
|
||||
:placeholder="
|
||||
$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TITLE.PLACEHOLDER')
|
||||
@@ -221,7 +224,7 @@ onMounted(getTeams);
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="formState.description"
|
||||
:style="{ ...inputStyles, padding: '8px 12px' }"
|
||||
:style="{ ...inputStyles, padding: '0.5rem 0.75rem' }"
|
||||
rows="3"
|
||||
class="text-sm"
|
||||
:placeholder="
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
|
||||
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -85,6 +87,7 @@ const linkIssue = async () => {
|
||||
searchQuery.value = '';
|
||||
issues.value = [];
|
||||
onClose();
|
||||
useTrack(LINEAR_EVENTS.LINK_ISSUE);
|
||||
} catch (error) {
|
||||
const errorMessage = parseLinearAPIErrorResponse(
|
||||
error,
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useI18n } from 'vue-i18n';
|
||||
import LinearAPI from 'dashboard/api/integrations/linear';
|
||||
import CreateOrLinkIssue from './CreateOrLinkIssue.vue';
|
||||
import Issue from './Issue.vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -56,6 +58,7 @@ const unlinkIssue = async linkId => {
|
||||
try {
|
||||
isUnlinking.value = true;
|
||||
await LinearAPI.unlinkIssue(linkId);
|
||||
useTrack(LINEAR_EVENTS.UNLINK_ISSUE);
|
||||
linkedIssue.value = null;
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.UNLINK.SUCCESS'));
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,41 +1,34 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useDetectKeyboardLayout } from 'dashboard/composables/useDetectKeyboardLayout';
|
||||
import { SHORTCUT_KEYS } from './constants';
|
||||
import { SHORTCUT_KEYS, KEYS } from './constants';
|
||||
import {
|
||||
LAYOUT_QWERTZ,
|
||||
keysToModifyInQWERTZ,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import Hotkey from 'dashboard/components/base/Hotkey.vue';
|
||||
|
||||
defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineProps({ show: Boolean });
|
||||
defineEmits(['close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const shortcutKeys = SHORTCUT_KEYS;
|
||||
const currentLayout = ref(null);
|
||||
|
||||
const title = item => t(`KEYBOARD_SHORTCUTS.TITLE.${item.label}`);
|
||||
const title = computed(
|
||||
() => item => t(`KEYBOARD_SHORTCUTS.TITLE.${item.label}`)
|
||||
);
|
||||
|
||||
// Added this function to check if the keySet needs a shift key
|
||||
// This is used to display the shift key in the modal
|
||||
// If the current layout is QWERTZ and the keySet contains a key that needs a shift key
|
||||
// If layout is QWERTZ then we add the Shift+keysToModify to fix an known issue
|
||||
// https://github.com/chatwoot/chatwoot/issues/9492
|
||||
const needsShiftKey = keySet => {
|
||||
return (
|
||||
const needsShiftKey = computed(
|
||||
() => keySet =>
|
||||
currentLayout.value === LAYOUT_QWERTZ &&
|
||||
keySet.some(key => keysToModifyInQWERTZ.has(key))
|
||||
);
|
||||
};
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
currentLayout.value = await useDetectKeyboardLayout();
|
||||
@@ -55,80 +48,46 @@ onMounted(async () => {
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 mb-1 ml-2">
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.WINDOWS_KEY_AND_COMMAND_KEY') }}
|
||||
{{ KEYS.WIN }}
|
||||
</Hotkey>
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[36px] key">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
|
||||
{{ KEYS.SLASH }}
|
||||
</Hotkey>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 px-8 pt-0 pb-8 gap-x-5 gap-y-3">
|
||||
<div class="flex justify-between items-center min-w-[25rem]">
|
||||
<h5 class="text-sm text-slate-800 dark:text-slate-100">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.TITLE.OPEN_CONVERSATION') }}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 mb-1 ml-2">
|
||||
<div class="flex gap-2">
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||
</Hotkey>
|
||||
<Hotkey custom-class="min-h-[28px] w-9 key"> {{ 'J' }} </Hotkey>
|
||||
<span
|
||||
class="flex items-center text-sm font-semibold text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.FORWARD_SLASH_KEY') }}
|
||||
</span>
|
||||
</div>
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||
</Hotkey>
|
||||
<Hotkey custom-class="w-9 key"> {{ 'K' }} </Hotkey>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center min-w-[25rem]">
|
||||
<h5 class="text-sm text-slate-800 dark:text-slate-100">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.TITLE.RESOLVE_AND_NEXT') }}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 mb-1 ml-2">
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.WINDOWS_KEY_AND_COMMAND_KEY') }}
|
||||
</Hotkey>
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.KEYS.ALT_OR_OPTION_KEY') }}
|
||||
</Hotkey>
|
||||
<Hotkey custom-class="w-9 key"> {{ 'E' }} </Hotkey>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="shortcutKey in shortcutKeys"
|
||||
:key="shortcutKey.id"
|
||||
v-for="shortcut in SHORTCUT_KEYS"
|
||||
:key="shortcut.id"
|
||||
class="flex justify-between items-center min-w-[25rem]"
|
||||
>
|
||||
<h5 class="text-sm text-slate-800 min-w-[36px] dark:text-slate-100">
|
||||
{{ title(shortcutKey) }}
|
||||
{{ title(shortcut) }}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 mb-1 ml-2">
|
||||
<Hotkey
|
||||
v-if="needsShiftKey(shortcutKey.keySet)"
|
||||
custom-class="min-h-[28px] min-w-[36px] key"
|
||||
>
|
||||
{{ 'Shift' }}
|
||||
</Hotkey>
|
||||
<Hotkey
|
||||
:class="{ 'min-w-[60px]': shortcutKey.firstKey !== 'Up' }"
|
||||
custom-class="min-h-[28px] normal-case key"
|
||||
>
|
||||
{{ shortcutKey.firstKey }}
|
||||
</Hotkey>
|
||||
<Hotkey
|
||||
:class="{ 'normal-case': shortcutKey.secondKey === 'Down' }"
|
||||
custom-class="min-h-[28px] min-w-[36px] key"
|
||||
>
|
||||
{{ shortcutKey.secondKey }}
|
||||
</Hotkey>
|
||||
<template v-if="needsShiftKey(shortcut.keySet)">
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[36px] key">
|
||||
{{ KEYS.SHIFT }}
|
||||
</Hotkey>
|
||||
</template>
|
||||
|
||||
<template v-for="(key, index) in shortcut.displayKeys" :key="index">
|
||||
<template v-if="key !== KEYS.SLASH">
|
||||
<Hotkey
|
||||
custom-class="min-h-[28px] min-w-[36px] key normal-case"
|
||||
>
|
||||
{{ key }}
|
||||
</Hotkey>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
class="flex items-center text-sm font-semibold text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,86 +1,95 @@
|
||||
export const KEYS = {
|
||||
ALT: 'Alt / ⌥',
|
||||
WIN: 'Win / ⌘',
|
||||
SHIFT: 'Shift',
|
||||
SLASH: '/',
|
||||
UP: 'Up',
|
||||
DOWN: 'Down',
|
||||
};
|
||||
|
||||
export const SHORTCUT_KEYS = [
|
||||
{
|
||||
id: 1,
|
||||
label: 'NAVIGATE_DROPDOWN',
|
||||
firstKey: 'Up',
|
||||
secondKey: 'Down',
|
||||
keySet: ['ArrowUp', 'ArrowDown'],
|
||||
label: 'OPEN_CONVERSATION',
|
||||
displayKeys: [KEYS.ALT, 'J', KEYS.SLASH, KEYS.ALT, 'K'],
|
||||
keySet: ['Alt+KeyJ', 'Alt+KeyK'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'RESOLVE_CONVERSATION',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'E',
|
||||
keySet: ['Alt+KeyE'],
|
||||
label: 'RESOLVE_AND_NEXT',
|
||||
displayKeys: [KEYS.WIN, KEYS.ALT, 'E'],
|
||||
keySet: ['$mod+Alt+KeyE'],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'GO_TO_CONVERSATION_DASHBOARD',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'C',
|
||||
keySet: ['Alt+KeyC'],
|
||||
label: 'NAVIGATE_DROPDOWN',
|
||||
displayKeys: [KEYS.UP, KEYS.DOWN],
|
||||
keySet: ['ArrowUp', 'ArrowDown'],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'ADD_ATTACHMENT',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'A',
|
||||
keySet: ['Alt+KeyA'],
|
||||
label: 'RESOLVE_CONVERSATION',
|
||||
displayKeys: [KEYS.ALT, 'E'],
|
||||
keySet: ['Alt+KeyE'],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: 'GO_TO_CONTACTS_DASHBOARD',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'V',
|
||||
keySet: ['Alt+KeyV'],
|
||||
label: 'GO_TO_CONVERSATION_DASHBOARD',
|
||||
displayKeys: [KEYS.ALT, 'C'],
|
||||
keySet: ['Alt+KeyC'],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: 'TOGGLE_SIDEBAR',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'O',
|
||||
keySet: ['Alt+KeyO'],
|
||||
label: 'ADD_ATTACHMENT',
|
||||
displayKeys: [KEYS.WIN, KEYS.ALT, 'A'],
|
||||
keySet: ['$mod+Alt+KeyA'],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: 'GO_TO_REPORTS_SIDEBAR',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'R',
|
||||
keySet: ['Alt+KeyR'],
|
||||
label: 'GO_TO_CONTACTS_DASHBOARD',
|
||||
displayKeys: [KEYS.ALT, 'V'],
|
||||
keySet: ['Alt+KeyV'],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
label: 'MOVE_TO_NEXT_TAB',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'N',
|
||||
keySet: ['Alt+KeyN'],
|
||||
label: 'TOGGLE_SIDEBAR',
|
||||
displayKeys: [KEYS.ALT, 'O'],
|
||||
keySet: ['Alt+KeyO'],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
label: 'GO_TO_SETTINGS',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'S',
|
||||
keySet: ['Alt+KeyS'],
|
||||
label: 'GO_TO_REPORTS_SIDEBAR',
|
||||
displayKeys: [KEYS.ALT, 'R'],
|
||||
keySet: ['Alt+KeyR'],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
label: 'MOVE_TO_NEXT_TAB',
|
||||
displayKeys: [KEYS.ALT, 'N'],
|
||||
keySet: ['Alt+KeyN'],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
label: 'SWITCH_TO_PRIVATE_NOTE',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'P',
|
||||
keySet: ['Alt+KeyP'],
|
||||
label: 'GO_TO_SETTINGS',
|
||||
displayKeys: [KEYS.ALT, 'S'],
|
||||
keySet: ['Alt+KeyS'],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
label: 'SWITCH_TO_REPLY',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'L',
|
||||
keySet: ['Alt+KeyL'],
|
||||
label: 'SWITCH_TO_PRIVATE_NOTE',
|
||||
displayKeys: [KEYS.ALT, 'P'],
|
||||
keySet: ['Alt+KeyP'],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
label: 'SWITCH_TO_REPLY',
|
||||
displayKeys: [KEYS.ALT, 'L'],
|
||||
keySet: ['Alt+KeyL'],
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
label: 'TOGGLE_SNOOZE_DROPDOWN',
|
||||
firstKey: 'Alt / ⌥',
|
||||
secondKey: 'M',
|
||||
displayKeys: [KEYS.ALT, 'M'],
|
||||
keySet: ['Alt+KeyM'],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user