Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<script>
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import Modal from '../../Modal.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
title: { type: String, default: '' },
|
||||
message: { type: String, default: '' },
|
||||
confirmText: { type: String, default: '' },
|
||||
rejectText: { type: String, default: '' },
|
||||
confirmValue: { type: String, default: '' },
|
||||
confirmPlaceHolderText: { type: String, default: '' },
|
||||
},
|
||||
emits: ['onClose', 'onConfirm', 'update:show'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
value: {
|
||||
required,
|
||||
isEqual(value) {
|
||||
// Trim whitespace from both input and target values
|
||||
const normalizedInput = (value || '').trim();
|
||||
const normalizedTarget = (this.confirmValue || '').trim();
|
||||
return normalizedInput === normalizedTarget;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
localShow: {
|
||||
get() {
|
||||
return this.show;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:show', value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.value = '';
|
||||
this.$emit('onClose');
|
||||
},
|
||||
onConfirm() {
|
||||
this.$emit('onConfirm');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:show="localShow" :on-close="closeModal">
|
||||
<woot-modal-header :header-title="title" :header-content="message" />
|
||||
<form @submit.prevent="onConfirm">
|
||||
<woot-input
|
||||
v-model="value"
|
||||
type="text"
|
||||
:class="{ error: v$.value.$error }"
|
||||
:placeholder="confirmPlaceHolderText"
|
||||
@blur="v$.value.$touch"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="rejectText"
|
||||
@click.prevent="closeModal"
|
||||
/>
|
||||
<NextButton
|
||||
ruby
|
||||
type="submit"
|
||||
:label="confirmText"
|
||||
:disabled="v$.value.$invalid"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script>
|
||||
import Modal from '../../Modal.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'This is a title',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'This is your description',
|
||||
},
|
||||
confirmLabel: {
|
||||
type: String,
|
||||
default: 'Yes',
|
||||
},
|
||||
cancelLabel: {
|
||||
type: String,
|
||||
default: 'No',
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
show: false,
|
||||
resolvePromise: undefined,
|
||||
rejectPromise: undefined,
|
||||
}),
|
||||
|
||||
methods: {
|
||||
showConfirmation() {
|
||||
this.show = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
},
|
||||
confirm() {
|
||||
this.resolvePromise(true);
|
||||
this.show = false;
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.resolvePromise(false);
|
||||
this.show = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:show="show" :on-close="cancel">
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<woot-modal-header :header-title="title" :header-content="description" />
|
||||
<div class="flex flex-row justify-end gap-2 py-4 px-6 w-full">
|
||||
<NextButton faded type="reset" :label="cancelLabel" @click="cancel" />
|
||||
<NextButton type="submit" :label="confirmLabel" @click="confirm" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import Modal from '../../Modal.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
onClose: { type: Function, default: () => {} },
|
||||
onConfirm: { type: Function, default: () => {} },
|
||||
title: { type: String, default: '' },
|
||||
message: { type: String, default: '' },
|
||||
messageValue: { type: String, default: '' },
|
||||
confirmText: { type: String, default: '' },
|
||||
rejectText: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const show = defineModel('show', { type: Boolean, default: false });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:show="show" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="title"
|
||||
:header-content="message"
|
||||
:header-content-value="messageValue"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2 p-8">
|
||||
<Button faded slate type="reset" :label="rejectText" @click="onClose" />
|
||||
<Button ruby type="submit" :label="confirmText" @click="onConfirm" />
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useDetectKeyboardLayout } from 'dashboard/composables/useDetectKeyboardLayout';
|
||||
import { SHORTCUT_KEYS, KEYS } from './constants';
|
||||
import {
|
||||
LAYOUT_QWERTZ,
|
||||
keysToModifyInQWERTZ,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import Hotkey from 'dashboard/components/base/Hotkey.vue';
|
||||
|
||||
defineProps({ show: Boolean });
|
||||
defineEmits(['close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const currentLayout = ref(null);
|
||||
|
||||
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 = computed(
|
||||
() => keySet =>
|
||||
currentLayout.value === LAYOUT_QWERTZ &&
|
||||
keySet.some(key => keysToModifyInQWERTZ.has(key))
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
currentLayout.value = await useDetectKeyboardLayout();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal :show="show" size="medium" :on-close="() => $emit('close')">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="$t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS')"
|
||||
/>
|
||||
<div class="grid grid-cols-2 px-8 pt-0 pb-4 mt-6 gap-x-5 gap-y-3">
|
||||
<div class="flex justify-between items-center min-w-[25rem]">
|
||||
<h5 class="text-sm text-n-slate-12">
|
||||
{{ $t('KEYBOARD_SHORTCUTS.TOGGLE_MODAL') }}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 mb-1 ml-2">
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[60px] normal-case key">
|
||||
{{ KEYS.WIN }}
|
||||
</Hotkey>
|
||||
<Hotkey custom-class="min-h-[28px] min-w-[36px] 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
|
||||
v-for="shortcut in SHORTCUT_KEYS"
|
||||
:key="shortcut.id"
|
||||
class="flex justify-between items-center min-w-[25rem]"
|
||||
>
|
||||
<h5 class="text-sm text-n-slate-12 min-w-[36px]">
|
||||
{{ title(shortcut) }}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2 mb-1 ml-2">
|
||||
<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-n-slate-12"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.key {
|
||||
@apply py-2 px-2.5 font-semibold text-xs text-n-slate-12 bg-n-slate-4 dark:bg-n-slate-2 shadow border-b-2 rtl:border-l-2 ltr:border-r-2 border-n-strong;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
export const KEYS = {
|
||||
ALT: 'Alt / ⌥',
|
||||
WIN: 'Win / ⌘',
|
||||
SHIFT: 'Shift',
|
||||
SLASH: '/',
|
||||
UP: 'Up',
|
||||
DOWN: 'Down',
|
||||
};
|
||||
|
||||
export const SHORTCUT_KEYS = [
|
||||
{
|
||||
id: 1,
|
||||
label: 'OPEN_CONVERSATION',
|
||||
displayKeys: [KEYS.ALT, 'J', KEYS.SLASH, KEYS.ALT, 'K'],
|
||||
keySet: ['Alt+KeyJ', 'Alt+KeyK'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'RESOLVE_AND_NEXT',
|
||||
displayKeys: [KEYS.WIN, KEYS.ALT, 'E'],
|
||||
keySet: ['$mod+Alt+KeyE'],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'NAVIGATE_DROPDOWN',
|
||||
displayKeys: [KEYS.UP, KEYS.DOWN],
|
||||
keySet: ['ArrowUp', 'ArrowDown'],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: 'RESOLVE_CONVERSATION',
|
||||
displayKeys: [KEYS.ALT, 'E'],
|
||||
keySet: ['Alt+KeyE'],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
label: 'GO_TO_CONVERSATION_DASHBOARD',
|
||||
displayKeys: [KEYS.ALT, 'C'],
|
||||
keySet: ['Alt+KeyC'],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
label: 'ADD_ATTACHMENT',
|
||||
displayKeys: [KEYS.WIN, KEYS.ALT, 'A'],
|
||||
keySet: ['$mod+Alt+KeyA'],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
label: 'GO_TO_CONTACTS_DASHBOARD',
|
||||
displayKeys: [KEYS.ALT, 'V'],
|
||||
keySet: ['Alt+KeyV'],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
label: 'TOGGLE_SIDEBAR',
|
||||
displayKeys: [KEYS.ALT, 'O'],
|
||||
keySet: ['Alt+KeyO'],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
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: 'GO_TO_SETTINGS',
|
||||
displayKeys: [KEYS.ALT, 'S'],
|
||||
keySet: ['Alt+KeyS'],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
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',
|
||||
displayKeys: [KEYS.ALT, 'M'],
|
||||
keySet: ['Alt+KeyM'],
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user