Merge branch 'develop' into fix/CW-3385
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
|
||||
import integrations from '../../api/integrations';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
|
||||
const isLoading = ref(true);
|
||||
const captainURL = ref('');
|
||||
const hasError = ref(false);
|
||||
|
||||
const loadCaptainFrame = async integration => {
|
||||
if (!integration || !integration.enabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const { data } = await integrations.fetchCaptainURL();
|
||||
captainURL.value = data.sso_url;
|
||||
} catch (error) {
|
||||
hasError.value = true;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const captainIntegration = computed(() =>
|
||||
getters['integrations/getIntegration'].value('captain', null)
|
||||
);
|
||||
|
||||
onMounted(() => loadCaptainFrame(captainIntegration.value));
|
||||
|
||||
watch(captainIntegration, updatedIntegration =>
|
||||
loadCaptainFrame(updatedIntegration)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 overflow-auto flex gap-8 flex-col font-inter text-slate-900 dark:text-slate-500"
|
||||
>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div v-if="!captainIntegration">
|
||||
{{ $t('INTEGRATION_SETTINGS.CAPTAIN.DISABLED') }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!captainIntegration.enabled"
|
||||
class="flex-1 flex flex-col gap-2 items-center justify-center"
|
||||
>
|
||||
<div>{{ $t('INTEGRATION_SETTINGS.CAPTAIN.DISABLED') }}</div>
|
||||
<router-link :to="{ name: 'settings_applications' }">
|
||||
<woot-button class="clear link">
|
||||
{{ $t('INTEGRATION_SETTINGS.CAPTAIN.CLICK_HERE_TO_CONFIGURE') }}
|
||||
</woot-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isLoading"
|
||||
class="flex-1 flex items-center justify-center"
|
||||
>
|
||||
<Spinner color-scheme="primary" />
|
||||
<span>{{ $t('INTEGRATION_SETTINGS.CAPTAIN.LOADING_CONSOLE') }}</span>
|
||||
</div>
|
||||
<div v-else-if="!isLoading && hasError">
|
||||
{{ $t('INTEGRATION_SETTINGS.CAPTAIN.FAILED_TO_LOAD_CONSOLE') }}
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="!isLoading && captainURL"
|
||||
:src="captainURL"
|
||||
class="w-full min-h-[800px] h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,55 +1,14 @@
|
||||
<template>
|
||||
<div
|
||||
class="app-wrapper h-full flex-grow-0 min-h-0 w-full max-w-full ml-auto mr-auto flex flex-wrap dark:text-slate-300"
|
||||
>
|
||||
<sidebar
|
||||
:route="currentRoute"
|
||||
:show-secondary-sidebar="isSidebarOpen"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
@toggle-account-modal="toggleAccountModal"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
@show-add-label-popup="showAddLabelPopup"
|
||||
/>
|
||||
<section class="flex h-full min-h-0 overflow-hidden flex-1 px-0">
|
||||
<router-view />
|
||||
<command-bar />
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
@show-create-account-modal="openCreateAccountModal"
|
||||
/>
|
||||
<add-account-modal
|
||||
:show="showCreateAccountModal"
|
||||
@close-account-create-modal="closeCreateAccountModal"
|
||||
/>
|
||||
<woot-key-shortcut-modal
|
||||
:show.sync="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<notification-panel
|
||||
v-if="isNotificationPanel"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||
<add-label-modal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sidebar from '../../components/layout/Sidebar.vue';
|
||||
import CommandBar from './commands/commandbar.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
|
||||
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
|
||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
|
||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
const CommandBar = () => import('./commands/commandbar.vue');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -61,7 +20,14 @@ export default {
|
||||
AddLabelModal,
|
||||
NotificationPanel,
|
||||
},
|
||||
mixins: [uiSettingsMixin],
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAccountModal: false,
|
||||
@@ -174,3 +140,44 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-wrap flex-grow-0 w-full h-full max-w-full min-h-0 ml-auto mr-auto app-wrapper dark:text-slate-300"
|
||||
>
|
||||
<Sidebar
|
||||
:route="currentRoute"
|
||||
:show-secondary-sidebar="isSidebarOpen"
|
||||
@openNotificationPanel="openNotificationPanel"
|
||||
@toggleAccountModal="toggleAccountModal"
|
||||
@openKeyShortcutModal="toggleKeyShortcutModal"
|
||||
@closeKeyShortcutModal="closeKeyShortcutModal"
|
||||
@showAddLabelPopup="showAddLabelPopup"
|
||||
/>
|
||||
<section class="flex flex-1 h-full min-h-0 px-0 overflow-hidden">
|
||||
<router-view />
|
||||
<CommandBar />
|
||||
<AccountSelector
|
||||
:show-account-modal="showAccountModal"
|
||||
@closeAccountModal="toggleAccountModal"
|
||||
@showCreateAccountModal="openCreateAccountModal"
|
||||
/>
|
||||
<AddAccountModal
|
||||
:show="showCreateAccountModal"
|
||||
@closeAccountCreateModal="closeCreateAccountModal"
|
||||
/>
|
||||
<WootKeyShortcutModal
|
||||
:show.sync="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<NotificationPanel
|
||||
v-if="isNotificationPanel"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||
<AddLabelModal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useEmitter } from 'dashboard/composables/emitter';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
const { t } = useI18n();
|
||||
const showCustomSnoozeModal = ref(false);
|
||||
|
||||
const selectedChat = computed(() => getters.getSelectedChat.value);
|
||||
const contextMenuChatId = computed(() => getters.getContextMenuChatId.value);
|
||||
|
||||
const toggleStatus = async (status, snoozedUntil) => {
|
||||
await store.dispatch('toggleStatus', {
|
||||
conversationId: selectedChat.value?.id || contextMenuChatId.value,
|
||||
status,
|
||||
snoozedUntil,
|
||||
});
|
||||
store.dispatch('setContextMenuChatId', null);
|
||||
useAlert(t('CONVERSATION.CHANGE_STATUS'));
|
||||
};
|
||||
|
||||
const onCmdSnoozeConversation = snoozeType => {
|
||||
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
showCustomSnoozeModal.value = true;
|
||||
} else {
|
||||
toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
findSnoozeTime(snoozeType) || null
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const chooseSnoozeTime = customSnoozeTime => {
|
||||
showCustomSnoozeModal.value = false;
|
||||
if (customSnoozeTime) {
|
||||
toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
getUnixTime(customSnoozeTime)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const hideCustomSnoozeModal = () => {
|
||||
// if we select custom snooze and the custom snooze modal is open
|
||||
// Then if the custom snooze modal is closed then set the context menu chat id to null
|
||||
store.dispatch('setContextMenuChatId', null);
|
||||
showCustomSnoozeModal.value = false;
|
||||
};
|
||||
|
||||
useEmitter(CMD_SNOOZE_CONVERSATION, onCmdSnoozeConversation);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<CustomSnoozeModal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@chooseTime="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</template>
|
||||
@@ -1,15 +1,3 @@
|
||||
<!-- eslint-disable vue/attribute-hyphenation -->
|
||||
<template>
|
||||
<ninja-keys
|
||||
ref="ninjakeys"
|
||||
:no-auto-load-md-icons="true"
|
||||
hideBreadcrumbs
|
||||
:placeholder="placeholder"
|
||||
@selected="onSelected"
|
||||
@closed="onClosed"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@chatwoot/ninja-keys';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
@@ -21,12 +9,10 @@ import appearanceHotKeys from './appearanceHotKeys';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
adminMixin,
|
||||
agentMixin,
|
||||
conversationHotKeysMixin,
|
||||
bulkActionsHotKeysMixin,
|
||||
@@ -110,6 +96,18 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/attribute-hyphenation -->
|
||||
<template>
|
||||
<ninja-keys
|
||||
ref="ninjakeys"
|
||||
noAutoLoadMdIcons
|
||||
hideBreadcrumbs
|
||||
:placeholder="placeholder"
|
||||
@selected="onSelected"
|
||||
@closed="onClosed"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
ninja-keys {
|
||||
--ninja-accent-color: var(--w-500);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from './CommandBarIcons';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
|
||||
const GO_TO_COMMANDS = [
|
||||
@@ -172,6 +173,12 @@ const GO_TO_COMMANDS = [
|
||||
];
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
|
||||
@@ -53,7 +53,7 @@ const INBOX_SNOOZE_EVENTS = [
|
||||
},
|
||||
{
|
||||
id: SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME,
|
||||
title: 'COMMAND_BAR.COMMANDS.CUSTOM',
|
||||
title: 'COMMAND_BAR.COMMANDS.UNTIL_CUSTOM_TIME',
|
||||
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
|
||||
parent: 'snooze_notification',
|
||||
icon: ICON_SNOOZE_NOTIFICATION,
|
||||
|
||||
@@ -1,3 +1,46 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: '',
|
||||
date: '',
|
||||
label: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
buttonDisabled() {
|
||||
return this.content && this.date === '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetValue() {
|
||||
this.content = '';
|
||||
this.date = '';
|
||||
},
|
||||
|
||||
optionSelected(event) {
|
||||
this.label = event.target.value;
|
||||
},
|
||||
|
||||
onAdd() {
|
||||
const task = {
|
||||
content: this.content,
|
||||
date: this.date,
|
||||
label: this.label,
|
||||
};
|
||||
this.$emit('add', task);
|
||||
this.resetValue();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="input-select-wrap">
|
||||
@@ -47,49 +90,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: '',
|
||||
date: '',
|
||||
label: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
buttonDisabled() {
|
||||
return this.content && this.date === '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetValue() {
|
||||
this.content = '';
|
||||
this.date = '';
|
||||
},
|
||||
|
||||
optionSelected(event) {
|
||||
this.label = event.target.value;
|
||||
},
|
||||
|
||||
onAdd() {
|
||||
const task = {
|
||||
content: this.content,
|
||||
date: this.date,
|
||||
label: this.label,
|
||||
};
|
||||
this.$emit('add', task);
|
||||
this.resetValue();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
display: flex;
|
||||
|
||||
+98
-84
@@ -1,84 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative w-1/4 h-full overflow-y-auto text-sm bg-white dark:bg-slate-900 border-slate-50 dark:border-slate-800/50"
|
||||
:class="showAvatar ? 'border-l border-solid ' : 'border-r border-solid'"
|
||||
>
|
||||
<contact-info
|
||||
:show-close-button="showCloseButton"
|
||||
:show-avatar="showAvatar"
|
||||
:contact="contact"
|
||||
close-icon-name="dismiss"
|
||||
@panel-close="onClose"
|
||||
@toggle-panel="onClose"
|
||||
/>
|
||||
<draggable
|
||||
:list="contactSidebarItems"
|
||||
:disabled="!dragEnabled"
|
||||
class="list-group"
|
||||
ghost-class="ghost"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<transition-group>
|
||||
<div
|
||||
v-for="element in contactSidebarItems"
|
||||
:key="element.name"
|
||||
class="list-group-item"
|
||||
>
|
||||
<div v-if="element.name === 'contact_attributes'">
|
||||
<accordion-item
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
||||
compact
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_ct_custom_attr_open', value)
|
||||
"
|
||||
>
|
||||
<custom-attributes
|
||||
:contact-id="contact.id"
|
||||
attribute-type="contact_attribute"
|
||||
attribute-class="conversation--attribute"
|
||||
attribute-from="contact_panel"
|
||||
:custom-attributes="contact.custom_attributes"
|
||||
:empty-state-message="
|
||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.NO_RECORDS_FOUND')
|
||||
"
|
||||
class="even"
|
||||
/>
|
||||
</accordion-item>
|
||||
</div>
|
||||
<div v-if="element.name === 'contact_labels'">
|
||||
<accordion-item
|
||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CONTACT_LABELS')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_labels_open')"
|
||||
@click="value => toggleSidebarUIState('is_ct_labels_open', value)"
|
||||
>
|
||||
<contact-label :contact-id="contact.id" class="contact-labels" />
|
||||
</accordion-item>
|
||||
</div>
|
||||
<div v-if="element.name === 'previous_conversation'">
|
||||
<accordion-item
|
||||
:title="
|
||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.PREVIOUS_CONVERSATIONS')
|
||||
"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_prev_conv_open')"
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_ct_prev_conv_open', value)
|
||||
"
|
||||
>
|
||||
<contact-conversations
|
||||
v-if="contact.id"
|
||||
:contact-id="contact.id"
|
||||
conversation-id=""
|
||||
/>
|
||||
</accordion-item>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
||||
import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations.vue';
|
||||
@@ -86,7 +5,7 @@ import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/Contact
|
||||
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
|
||||
import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -95,9 +14,8 @@ export default {
|
||||
ContactInfo,
|
||||
ContactLabel,
|
||||
CustomAttributes,
|
||||
draggable,
|
||||
Draggable: draggable,
|
||||
},
|
||||
mixins: [uiSettingsMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
@@ -116,6 +34,21 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
contactSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
} = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
contactSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragEnabled: true,
|
||||
@@ -143,6 +76,87 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative w-1/4 h-full overflow-y-auto text-sm bg-white dark:bg-slate-900 border-slate-50 dark:border-slate-800/50"
|
||||
:class="showAvatar ? 'border-l border-solid ' : 'border-r border-solid'"
|
||||
>
|
||||
<ContactInfo
|
||||
:show-close-button="showCloseButton"
|
||||
:show-avatar="showAvatar"
|
||||
:contact="contact"
|
||||
close-icon-name="dismiss"
|
||||
@panelClose="onClose"
|
||||
@togglePanel="onClose"
|
||||
/>
|
||||
<Draggable
|
||||
:list="contactSidebarItems"
|
||||
:disabled="!dragEnabled"
|
||||
class="list-group"
|
||||
ghost-class="ghost"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<transition-group>
|
||||
<div
|
||||
v-for="element in contactSidebarItems"
|
||||
:key="element.name"
|
||||
class="list-group-item"
|
||||
>
|
||||
<div v-if="element.name === 'contact_attributes'">
|
||||
<AccordionItem
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_custom_attr_open')"
|
||||
compact
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_ct_custom_attr_open', value)
|
||||
"
|
||||
>
|
||||
<CustomAttributes
|
||||
:contact-id="contact.id"
|
||||
attribute-type="contact_attribute"
|
||||
attribute-class="conversation--attribute"
|
||||
attribute-from="contact_panel"
|
||||
:custom-attributes="contact.custom_attributes"
|
||||
:empty-state-message="
|
||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.NO_RECORDS_FOUND')
|
||||
"
|
||||
class="even"
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<div v-if="element.name === 'contact_labels'">
|
||||
<AccordionItem
|
||||
:title="$t('CONTACT_PANEL.SIDEBAR_SECTIONS.CONTACT_LABELS')"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_labels_open')"
|
||||
@click="value => toggleSidebarUIState('is_ct_labels_open', value)"
|
||||
>
|
||||
<ContactLabel :contact-id="contact.id" class="contact-labels" />
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<div v-if="element.name === 'previous_conversation'">
|
||||
<AccordionItem
|
||||
:title="
|
||||
$t('CONTACT_PANEL.SIDEBAR_SECTIONS.PREVIOUS_CONVERSATIONS')
|
||||
"
|
||||
:is-open="isContactSidebarItemOpen('is_ct_prev_conv_open')"
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_ct_prev_conv_open', value)
|
||||
"
|
||||
>
|
||||
<ContactConversations
|
||||
v-if="contact.id"
|
||||
:contact-id="contact.id"
|
||||
conversation-id=""
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</Draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.contact--profile {
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
<template>
|
||||
<label-selector
|
||||
:all-labels="allLabels"
|
||||
:saved-labels="savedLabels"
|
||||
@add="addItem"
|
||||
@remove="removeItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: { LabelSelector },
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
contactId: {
|
||||
type: [String, Number],
|
||||
@@ -33,7 +23,6 @@ export default {
|
||||
},
|
||||
|
||||
...mapGetters({
|
||||
labelUiFlags: 'contactLabels/getUIFlags',
|
||||
allLabels: 'labels/getLabels',
|
||||
}),
|
||||
},
|
||||
@@ -59,7 +48,7 @@ export default {
|
||||
labels: selectedLabels,
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
|
||||
useAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -86,4 +75,11 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<LabelSelector
|
||||
:all-labels="allLabels"
|
||||
:saved-labels="savedLabels"
|
||||
@add="addItem"
|
||||
@remove="removeItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+113
-123
@@ -1,96 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
||||
<p class="text-slate-600 dark:text-slate-200">
|
||||
{{ filterModalSubTitle }}
|
||||
</p>
|
||||
</woot-modal-header>
|
||||
<div class="p-8">
|
||||
<div v-if="isSegmentsView">
|
||||
<label class="input-label" :class="{ error: !activeSegmentNewName }">
|
||||
{{ $t('CONTACTS_FILTER.SEGMENT_LABEL') }}
|
||||
<input
|
||||
v-model="activeSegmentNewName"
|
||||
type="text"
|
||||
class="folder-input border-slate-75 dark:border-slate-600 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100"
|
||||
/>
|
||||
<span v-if="!activeSegmentNewName" class="message">
|
||||
{{ $t('CONTACTS_FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-label">
|
||||
{{ $t('CONTACTS_FILTER.SEGMENT_QUERY_LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 rounded-lg bg-slate-25 dark:bg-slate-900 border border-solid border-slate-50 dark:border-slate-700/50 mb-4"
|
||||
>
|
||||
<filter-input-box
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
:key="i"
|
||||
v-model="appliedFilters[i]"
|
||||
:filter-groups="filterGroups"
|
||||
:grouped-filters="true"
|
||||
:input-type="
|
||||
getInputType(
|
||||
appliedFilters[i].attribute_key,
|
||||
appliedFilters[i].filter_operator
|
||||
)
|
||||
"
|
||||
:operators="getOperators(appliedFilters[i].attribute_key)"
|
||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
:v="$v.appliedFilters.$each[i]"
|
||||
@resetFilter="resetFilter(i, appliedFilters[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
icon="add"
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewFilter"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.ADD_NEW_FILTER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasAppliedFilters && !isSegmentsView"
|
||||
icon="subtract"
|
||||
color-scheme="alert"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="clearFilters"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.CLEAR_ALL_FILTERS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CONTACTS_FILTER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="isSegmentsView"
|
||||
:disabled="!activeSegmentNewName"
|
||||
@click="updateSegment"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.UPDATE_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button v-else @click="submitFilterQuery">
|
||||
{{ $t('CONTACTS_FILTER.SUBMIT_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -98,11 +7,13 @@ import { filterAttributeGroups } from '../contactFilterItems';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import { validateConversationOrContactFilters } from 'dashboard/helper/validations.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
mixins: [alertMixin, filterMixin],
|
||||
mixins: [filterMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -125,22 +36,6 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
appliedFilters: {
|
||||
required,
|
||||
$each: {
|
||||
values: {
|
||||
required,
|
||||
ensureBetween0to999(value, prop) {
|
||||
if (prop.filter_operator === 'days_before') {
|
||||
return parseInt(value, 10) > 0 && parseInt(value, 10) < 999;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
@@ -152,6 +47,7 @@ export default {
|
||||
filterAttributeGroups,
|
||||
attributeModel: 'contact_attribute',
|
||||
filtersFori18n: 'CONTACTS_FILTER',
|
||||
validationErrors: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -308,26 +204,29 @@ export default {
|
||||
},
|
||||
removeFilter(index) {
|
||||
if (this.appliedFilters.length <= 1) {
|
||||
this.showAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
|
||||
useAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
|
||||
} else {
|
||||
this.appliedFilters.splice(index, 1);
|
||||
}
|
||||
},
|
||||
submitFilterQuery() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) return;
|
||||
this.$store.dispatch(
|
||||
'contacts/setContactFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
this.validationErrors = validateConversationOrContactFilters(
|
||||
this.appliedFilters
|
||||
);
|
||||
this.$emit('applyFilter', this.appliedFilters);
|
||||
this.$track(CONTACTS_EVENTS.APPLY_FILTER, {
|
||||
applied_filters: this.appliedFilters.map(filter => ({
|
||||
key: filter.attribute_key,
|
||||
operator: filter.filter_operator,
|
||||
query_operator: filter.query_operator,
|
||||
})),
|
||||
});
|
||||
if (Object.keys(this.validationErrors).length === 0) {
|
||||
this.$store.dispatch(
|
||||
'contacts/setContactFilters',
|
||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
||||
);
|
||||
this.$emit('applyFilter', this.appliedFilters);
|
||||
this.$track(CONTACTS_EVENTS.APPLY_FILTER, {
|
||||
applied_filters: this.appliedFilters.map(filter => ({
|
||||
key: filter.attribute_key,
|
||||
operator: filter.filter_operator,
|
||||
query_operator: filter.query_operator,
|
||||
})),
|
||||
});
|
||||
}
|
||||
},
|
||||
updateSegment() {
|
||||
this.$emit(
|
||||
@@ -354,6 +253,97 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
||||
<p class="text-slate-600 dark:text-slate-200">
|
||||
{{ filterModalSubTitle }}
|
||||
</p>
|
||||
</woot-modal-header>
|
||||
<div class="p-8">
|
||||
<div v-if="isSegmentsView">
|
||||
<label class="input-label" :class="{ error: !activeSegmentNewName }">
|
||||
{{ $t('CONTACTS_FILTER.SEGMENT_LABEL') }}
|
||||
<input
|
||||
v-model="activeSegmentNewName"
|
||||
type="text"
|
||||
class="bg-white folder-input border-slate-75 dark:border-slate-600 dark:bg-slate-900 text-slate-900 dark:text-slate-100"
|
||||
/>
|
||||
<span v-if="!activeSegmentNewName" class="message">
|
||||
{{ $t('CONTACTS_FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-label">
|
||||
{{ $t('CONTACTS_FILTER.SEGMENT_QUERY_LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="p-4 mb-4 border border-solid rounded-lg bg-slate-25 dark:bg-slate-900 border-slate-50 dark:border-slate-700/50"
|
||||
>
|
||||
<FilterInputBox
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
:key="i"
|
||||
v-model="appliedFilters[i]"
|
||||
:filter-groups="filterGroups"
|
||||
grouped-filters
|
||||
:input-type="
|
||||
getInputType(
|
||||
appliedFilters[i].attribute_key,
|
||||
appliedFilters[i].filter_operator
|
||||
)
|
||||
"
|
||||
:operators="getOperators(appliedFilters[i].attribute_key)"
|
||||
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
|
||||
:show-query-operator="i !== appliedFilters.length - 1"
|
||||
:show-user-input="showUserInput(appliedFilters[i].filter_operator)"
|
||||
:error-message="validationErrors[`filter_${i}`]"
|
||||
@resetFilter="resetFilter(i, appliedFilters[i])"
|
||||
@removeFilter="removeFilter(i)"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<woot-button
|
||||
icon="add"
|
||||
color-scheme="success"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="appendNewFilter"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.ADD_NEW_FILTER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasAppliedFilters && !isSegmentsView"
|
||||
icon="subtract"
|
||||
color-scheme="alert"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="clearFilters"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.CLEAR_ALL_FILTERS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CONTACTS_FILTER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="isSegmentsView"
|
||||
:disabled="!activeSegmentNewName"
|
||||
@click="updateSegment"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.UPDATE_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button v-else @click="submitFilterQuery">
|
||||
{{ $t('CONTACTS_FILTER.SUBMIT_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.folder-input {
|
||||
@apply w-[50%];
|
||||
|
||||
@@ -1,32 +1,3 @@
|
||||
<template>
|
||||
<section
|
||||
class="contacts-table-wrap bg-white dark:bg-slate-900 flex-1 h-full overflow-hidden -mt-1"
|
||||
>
|
||||
<ve-table
|
||||
:fixed-header="true"
|
||||
max-height="calc(100vh - 7.125rem)"
|
||||
scroll-width="187rem"
|
||||
:columns="columns"
|
||||
:table-data="tableData"
|
||||
:border-around="false"
|
||||
:sort-option="sortOption"
|
||||
/>
|
||||
|
||||
<empty-state
|
||||
v-if="showSearchEmptyState"
|
||||
:title="$t('CONTACTS_PAGE.LIST.404')"
|
||||
/>
|
||||
<empty-state
|
||||
v-else-if="!isLoading && !contacts.length"
|
||||
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
||||
/>
|
||||
<div v-if="isLoading" class="items-center flex text-base justify-center">
|
||||
<spinner />
|
||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VeTable } from 'vue-easytable';
|
||||
import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
@@ -34,7 +5,7 @@ import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
@@ -44,7 +15,7 @@ export default {
|
||||
Spinner,
|
||||
VeTable,
|
||||
},
|
||||
mixins: [timeMixin, rtlMixin],
|
||||
mixins: [rtlMixin],
|
||||
props: {
|
||||
contacts: {
|
||||
type: Array,
|
||||
@@ -62,10 +33,6 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
activeContactId: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
sortParam: {
|
||||
type: String,
|
||||
default: 'last_activity_at',
|
||||
@@ -80,7 +47,7 @@ export default {
|
||||
sortConfig: {},
|
||||
sortOption: {
|
||||
sortAlways: true,
|
||||
sortChange: params => this.$emit('on-sort-change', params),
|
||||
sortChange: params => this.$emit('onSortChange', params),
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -105,9 +72,9 @@ export default {
|
||||
countryCode: additional.country_code,
|
||||
conversationsCount: item.conversations_count || '---',
|
||||
last_activity_at: lastActivityAt
|
||||
? this.dynamicTime(lastActivityAt)
|
||||
? dynamicTime(lastActivityAt)
|
||||
: '---',
|
||||
created_at: createdAt ? this.dynamicTime(createdAt) : '---',
|
||||
created_at: createdAt ? dynamicTime(createdAt) : '---',
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -134,7 +101,7 @@ export default {
|
||||
status={row.availability_status}
|
||||
/>
|
||||
<div class="user-block">
|
||||
<h6 class="text-base overflow-hidden whitespace-nowrap text-ellipsis">
|
||||
<h6 class="overflow-hidden text-base whitespace-nowrap text-ellipsis">
|
||||
<router-link
|
||||
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
|
||||
class="user-name"
|
||||
@@ -277,6 +244,35 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex-1 h-full -mt-1 overflow-hidden bg-white contacts-table-wrap dark:bg-slate-900"
|
||||
>
|
||||
<VeTable
|
||||
fixed-header
|
||||
max-height="calc(100vh - 7.125rem)"
|
||||
scroll-width="187rem"
|
||||
:columns="columns"
|
||||
:table-data="tableData"
|
||||
:border-around="false"
|
||||
:sort-option="sortOption"
|
||||
/>
|
||||
|
||||
<EmptyState
|
||||
v-if="showSearchEmptyState"
|
||||
:title="$t('CONTACTS_PAGE.LIST.404')"
|
||||
/>
|
||||
<EmptyState
|
||||
v-else-if="!isLoading && !contacts.length"
|
||||
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
||||
/>
|
||||
<div v-if="isLoading" class="flex items-center justify-center text-base">
|
||||
<Spinner />
|
||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contacts-table-wrap::v-deep {
|
||||
.ve-table {
|
||||
|
||||
@@ -1,86 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-col h-full" :class="wrapClass">
|
||||
<contacts-header
|
||||
:search-query="searchQuery"
|
||||
:header-title="pageTitle"
|
||||
:segments-id="segmentsId"
|
||||
this-selected-contact-id=""
|
||||
@on-input-search="onInputSearch"
|
||||
@on-toggle-create="onToggleCreate"
|
||||
@on-toggle-filter="onToggleFilters"
|
||||
@on-search-submit="onSearchSubmit"
|
||||
@on-toggle-import="onToggleImport"
|
||||
@on-export-submit="onExportSubmit"
|
||||
@on-toggle-save-filter="onToggleSaveFilters"
|
||||
@on-toggle-delete-filter="onToggleDeleteFilters"
|
||||
@on-toggle-edit-filter="onToggleFilters"
|
||||
/>
|
||||
<contacts-table
|
||||
:contacts="records"
|
||||
:show-search-empty-state="showEmptySearchResult"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:on-click-contact="openContactInfoPanel"
|
||||
:active-contact-id="selectedContactId"
|
||||
@on-sort-change="onSortChange"
|
||||
/>
|
||||
<table-footer
|
||||
class="border-t border-slate-75 dark:border-slate-700/50"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="meta.count"
|
||||
:page-size="15"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<add-custom-views
|
||||
v-if="showAddSegmentsModal"
|
||||
:custom-views-query="segmentsQuery"
|
||||
:filter-type="filterType"
|
||||
:open-last-saved-item="openSavedItemInSegment"
|
||||
@close="onCloseAddSegmentsModal"
|
||||
/>
|
||||
<delete-custom-views
|
||||
v-if="showDeleteSegmentsModal"
|
||||
:show-delete-popup.sync="showDeleteSegmentsModal"
|
||||
:active-custom-view="activeSegment"
|
||||
:custom-views-id="segmentsId"
|
||||
:active-filter-type="filterType"
|
||||
:open-last-item-after-delete="openLastItemAfterDeleteInSegment"
|
||||
@close="onCloseDeleteSegmentsModal"
|
||||
/>
|
||||
|
||||
<contact-info-panel
|
||||
v-if="showContactViewPane"
|
||||
:contact="selectedContact"
|
||||
:on-close="closeContactInfoPanel"
|
||||
/>
|
||||
<create-contact :show="showCreateModal" @cancel="onToggleCreate" />
|
||||
<woot-modal :show.sync="showImportModal" :on-close="onToggleImport">
|
||||
<import-contacts v-if="showImportModal" :on-close="onToggleImport" />
|
||||
</woot-modal>
|
||||
<woot-modal
|
||||
:show.sync="showFiltersModal"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
size="medium"
|
||||
>
|
||||
<contacts-advanced-filters
|
||||
v-if="showFiltersModal"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
:initial-filter-types="contactFilterItems"
|
||||
:initial-applied-filters="appliedFilter"
|
||||
:active-segment-name="activeSegmentName"
|
||||
:is-segments-view="hasActiveSegments"
|
||||
@applyFilter="onApplyFilter"
|
||||
@updateSegment="onUpdateSegment"
|
||||
@clearFilters="clearFilters"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import ContactsHeader from './Header.vue';
|
||||
import ContactsTable from './ContactsTable.vue';
|
||||
@@ -94,7 +14,6 @@ import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
|
||||
@@ -113,7 +32,6 @@ export default {
|
||||
AddCustomViews,
|
||||
DeleteCustomViews,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
label: { type: String, default: '' },
|
||||
segmentsId: {
|
||||
@@ -404,11 +322,9 @@ export default {
|
||||
...query,
|
||||
label: this.label,
|
||||
});
|
||||
this.showAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
useAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE')
|
||||
);
|
||||
useAlert(error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
setParamsForEditSegmentModal() {
|
||||
@@ -471,3 +387,84 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="flex flex-col h-full" :class="wrapClass">
|
||||
<ContactsHeader
|
||||
:search-query="searchQuery"
|
||||
:header-title="pageTitle"
|
||||
:segments-id="segmentsId"
|
||||
this-selected-contact-id=""
|
||||
@onInputSearch="onInputSearch"
|
||||
@onToggleCreate="onToggleCreate"
|
||||
@onToggleFilter="onToggleFilters"
|
||||
@onSearchSubmit="onSearchSubmit"
|
||||
@onToggleImport="onToggleImport"
|
||||
@onExportSubmit="onExportSubmit"
|
||||
@onToggleSaveFilter="onToggleSaveFilters"
|
||||
@onToggleDeleteFilter="onToggleDeleteFilters"
|
||||
@onToggleEditFilter="onToggleFilters"
|
||||
/>
|
||||
<ContactsTable
|
||||
:contacts="records"
|
||||
:show-search-empty-state="showEmptySearchResult"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:on-click-contact="openContactInfoPanel"
|
||||
:active-contact-id="selectedContactId"
|
||||
@onSortChange="onSortChange"
|
||||
/>
|
||||
<TableFooter
|
||||
class="border-t border-slate-75 dark:border-slate-700/50"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="meta.count"
|
||||
:page-size="15"
|
||||
@pageChange="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AddCustomViews
|
||||
v-if="showAddSegmentsModal"
|
||||
:custom-views-query="segmentsQuery"
|
||||
:filter-type="filterType"
|
||||
:open-last-saved-item="openSavedItemInSegment"
|
||||
@close="onCloseAddSegmentsModal"
|
||||
/>
|
||||
<DeleteCustomViews
|
||||
v-if="showDeleteSegmentsModal"
|
||||
:show-delete-popup.sync="showDeleteSegmentsModal"
|
||||
:active-custom-view="activeSegment"
|
||||
:custom-views-id="segmentsId"
|
||||
:active-filter-type="filterType"
|
||||
:open-last-item-after-delete="openLastItemAfterDeleteInSegment"
|
||||
@close="onCloseDeleteSegmentsModal"
|
||||
/>
|
||||
|
||||
<ContactInfoPanel
|
||||
v-if="showContactViewPane"
|
||||
:contact="selectedContact"
|
||||
:on-close="closeContactInfoPanel"
|
||||
/>
|
||||
<CreateContact :show="showCreateModal" @cancel="onToggleCreate" />
|
||||
<woot-modal :show.sync="showImportModal" :on-close="onToggleImport">
|
||||
<ImportContacts v-if="showImportModal" :on-close="onToggleImport" />
|
||||
</woot-modal>
|
||||
<woot-modal
|
||||
:show.sync="showFiltersModal"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
size="medium"
|
||||
>
|
||||
<ContactsAdvancedFilters
|
||||
v-if="showFiltersModal"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
:initial-filter-types="contactFilterItems"
|
||||
:initial-applied-filters="appliedFilter"
|
||||
:active-segment-name="activeSegmentName"
|
||||
:is-segments-view="hasActiveSegments"
|
||||
@applyFilter="onApplyFilter"
|
||||
@updateSegment="onUpdateSegment"
|
||||
@clearFilters="clearFilters"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,134 +1,8 @@
|
||||
<template>
|
||||
<header
|
||||
class="bg-white border-b dark:bg-slate-900 border-slate-50 dark:border-slate-800"
|
||||
>
|
||||
<div class="flex justify-between w-full px-4 py-2">
|
||||
<div class="flex items-center justify-center max-w-full min-w-[6.25rem]">
|
||||
<woot-sidemenu-icon />
|
||||
<h1
|
||||
class="m-0 mx-2 my-0 overflow-hidden text-xl text-slate-900 dark:text-slate-100 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div
|
||||
class="max-w-[400px] min-w-[150px] flex items-center relative mx-2 search-wrap"
|
||||
>
|
||||
<div class="flex items-center absolute h-full left-2.5">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
class="h-5 text-sm leading-9 text-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="$t('CONTACTS_PAGE.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="contact-search border-slate-100 dark:border-slate-600"
|
||||
:value="searchQuery"
|
||||
@keyup.enter="submitSearch"
|
||||
@input="inputSearch"
|
||||
/>
|
||||
<woot-button
|
||||
:is-loading="false"
|
||||
class="clear"
|
||||
:class-names="searchButtonClass"
|
||||
@click="submitSearch"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="hasActiveSegments" class="flex gap-2">
|
||||
<woot-button
|
||||
class="clear"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="onToggleEditSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_EDIT') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
class="clear"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
@click="onToggleDeleteSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="!hasActiveSegments" class="relative">
|
||||
<div
|
||||
v-if="hasAppliedFilters"
|
||||
class="absolute w-2 h-2 rounded-full top-1 right-3 bg-slate-500 dark:bg-slate-500"
|
||||
/>
|
||||
<woot-button
|
||||
class="clear"
|
||||
color-scheme="secondary"
|
||||
data-testid="create-new-contact"
|
||||
icon="filter"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
<woot-button
|
||||
v-if="hasAppliedFilters && !hasActiveSegments"
|
||||
class="clear"
|
||||
color-scheme="alert"
|
||||
variant="clear"
|
||||
icon="save"
|
||||
@click="onToggleSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_SAVE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
class="clear"
|
||||
color-scheme="success"
|
||||
icon="person-add"
|
||||
data-testid="create-new-contact"
|
||||
@click="toggleCreate"
|
||||
>
|
||||
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
color-scheme="info"
|
||||
icon="upload"
|
||||
class="clear"
|
||||
@click="toggleImport"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
color-scheme="info"
|
||||
icon="download"
|
||||
class="clear"
|
||||
@click="submitExport"
|
||||
>
|
||||
{{ $t('EXPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<woot-confirm-modal
|
||||
ref="confirmExportContactsDialog"
|
||||
:title="$t('EXPORT_CONTACTS.CONFIRM.TITLE')"
|
||||
:description="exportDescription"
|
||||
:confirm-label="$t('EXPORT_CONTACTS.CONFIRM.YES')"
|
||||
:cancel-label="$t('EXPORT_CONTACTS.CONFIRM.NO')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
export default {
|
||||
mixins: [adminMixin],
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
@@ -143,6 +17,12 @@ export default {
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreateModal: false,
|
||||
@@ -151,7 +31,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
searchButtonClass() {
|
||||
return this.searchQuery !== '' ? 'show' : '';
|
||||
return this.searchQuery !== ''
|
||||
? 'opacity-100 translate-x-0 visible'
|
||||
: '-translate-x-px opacity-0 invisible';
|
||||
},
|
||||
...mapGetters({
|
||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
||||
@@ -170,54 +52,168 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onToggleSegmentsModal() {
|
||||
this.$emit('on-toggle-save-filter');
|
||||
this.$emit('onToggleSaveFilter');
|
||||
},
|
||||
onToggleEditSegmentsModal() {
|
||||
this.$emit('on-toggle-edit-filter');
|
||||
this.$emit('onToggleEditFilter');
|
||||
},
|
||||
onToggleDeleteSegmentsModal() {
|
||||
this.$emit('on-toggle-delete-filter');
|
||||
this.$emit('onToggleDeleteFilter');
|
||||
},
|
||||
toggleCreate() {
|
||||
this.$emit('on-toggle-create');
|
||||
this.$emit('onToggleCreate');
|
||||
},
|
||||
toggleFilter() {
|
||||
this.$emit('on-toggle-filter');
|
||||
this.$emit('onToggleFilter');
|
||||
},
|
||||
toggleImport() {
|
||||
this.$emit('on-toggle-import');
|
||||
this.$emit('onToggleImport');
|
||||
},
|
||||
async submitExport() {
|
||||
const ok =
|
||||
await this.$refs.confirmExportContactsDialog.showConfirmation();
|
||||
|
||||
if (ok) {
|
||||
this.$emit('on-export-submit');
|
||||
this.$emit('onExportSubmit');
|
||||
}
|
||||
},
|
||||
submitSearch() {
|
||||
this.$emit('on-search-submit');
|
||||
this.$emit('onSearchSubmit');
|
||||
},
|
||||
inputSearch(event) {
|
||||
this.$emit('on-input-search', event);
|
||||
this.$emit('onInputSearch', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-wrap {
|
||||
.contact-search {
|
||||
@apply pl-9 pr-[3.75rem] text-sm w-full h-[2.375rem] m-0;
|
||||
}
|
||||
<template>
|
||||
<header
|
||||
class="bg-white border-b dark:bg-slate-900 border-slate-50 dark:border-slate-800"
|
||||
>
|
||||
<div class="flex justify-between w-full px-4 py-2">
|
||||
<div class="flex flex-col items-center w-full gap-2 md:flex-row">
|
||||
<div class="flex justify-between w-full gap-2">
|
||||
<div
|
||||
class="flex items-center justify-center max-w-full min-w-[6.25rem]"
|
||||
>
|
||||
<woot-sidemenu-icon />
|
||||
<h1
|
||||
class="m-0 mx-2 my-0 overflow-hidden text-xl text-slate-900 dark:text-slate-100 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="max-w-[400px] min-w-[100px] flex items-center relative mx-2"
|
||||
>
|
||||
<div class="flex items-center absolute h-full left-2.5">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
class="h-5 text-sm leading-9 text-slate-700 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="$t('CONTACTS_PAGE.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="!pl-9 !pr-[3.75rem] !text-sm !w-full !h-[2.375rem] !m-0 border-slate-100 dark:border-slate-600"
|
||||
:value="searchQuery"
|
||||
@keyup.enter="submitSearch"
|
||||
@input="inputSearch"
|
||||
/>
|
||||
<woot-button
|
||||
:is-loading="false"
|
||||
class="absolute h-8 px-2 py-0 ml-2 transition-transform duration-100 ease-linear clear right-1"
|
||||
:class-names="searchButtonClass"
|
||||
@click="submitSearch"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<div v-if="hasActiveSegments" class="flex gap-2">
|
||||
<woot-button
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="onToggleEditSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_EDIT') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
@click="onToggleDeleteSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="!hasActiveSegments" class="relative">
|
||||
<div
|
||||
v-if="hasAppliedFilters"
|
||||
class="absolute w-2 h-2 rounded-full top-1 right-3 bg-slate-500 dark:bg-slate-500"
|
||||
/>
|
||||
<woot-button
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="secondary"
|
||||
data-testid="create-new-contact"
|
||||
icon="filter"
|
||||
@click="toggleFilter"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
.button {
|
||||
transition: transform 100ms linear;
|
||||
@apply ml-2 h-8 right-1 absolute py-0 px-2 opacity-0 -translate-x-px invisible;
|
||||
}
|
||||
<woot-button
|
||||
v-if="hasAppliedFilters && !hasActiveSegments"
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="alert"
|
||||
variant="clear"
|
||||
icon="save"
|
||||
@click="onToggleSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_SAVE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
color-scheme="success"
|
||||
icon="person-add"
|
||||
data-testid="create-new-contact"
|
||||
@click="toggleCreate"
|
||||
>
|
||||
{{ $t('CREATE_CONTACT.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
.button.show {
|
||||
@apply opacity-100 translate-x-0 visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
color-scheme="info"
|
||||
icon="upload"
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
@click="toggleImport"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
color-scheme="info"
|
||||
icon="download"
|
||||
class="clear [&>span]:hidden xs:[&>span]:block"
|
||||
@click="submitExport"
|
||||
>
|
||||
{{ $t('EXPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-confirm-modal
|
||||
ref="confirmExportContactsDialog"
|
||||
:title="$t('EXPORT_CONTACTS.CONFIRM.TITLE')"
|
||||
:description="exportDescription"
|
||||
:confirm-label="$t('EXPORT_CONTACTS.CONFIRM.YES')"
|
||||
:cancel-label="$t('EXPORT_CONTACTS.CONFIRM.NO')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -1,57 +1,13 @@
|
||||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
|
||||
<p>
|
||||
{{ $t('IMPORT_CONTACTS.DESC') }}
|
||||
<a :href="csvUrl" download="import-contacts-sample">{{
|
||||
$t('IMPORT_CONTACTS.DOWNLOAD_LABEL')
|
||||
}}</a>
|
||||
</p>
|
||||
</woot-modal-header>
|
||||
<div class="flex flex-col p-8">
|
||||
<div class="w-full">
|
||||
<label>
|
||||
<span>{{ $t('IMPORT_CONTACTS.FORM.LABEL') }}</span>
|
||||
<input
|
||||
id="file"
|
||||
ref="file"
|
||||
type="file"
|
||||
accept="text/csv"
|
||||
@change="handleFileUpload"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<div class="w-full">
|
||||
<woot-button
|
||||
:disabled="uiFlags.isCreating || !file"
|
||||
:loading="uiFlags.isCreating"
|
||||
@click="uploadFile"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('IMPORT_CONTACTS.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from '../../../../components/Modal.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import Modal from '../../../../components/Modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -81,12 +37,10 @@ export default {
|
||||
if (!this.file) return;
|
||||
await this.$store.dispatch('contacts/import', this.file);
|
||||
this.onClose();
|
||||
this.showAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
useAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
this.$track(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE')
|
||||
);
|
||||
useAlert(error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE'));
|
||||
this.$track(CONTACTS_EVENTS.IMPORT_FAILURE);
|
||||
}
|
||||
},
|
||||
@@ -96,3 +50,46 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :show.sync="show" :on-close="onClose">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
|
||||
<p>
|
||||
{{ $t('IMPORT_CONTACTS.DESC') }}
|
||||
<a :href="csvUrl" download="import-contacts-sample">{{
|
||||
$t('IMPORT_CONTACTS.DOWNLOAD_LABEL')
|
||||
}}</a>
|
||||
</p>
|
||||
</woot-modal-header>
|
||||
<div class="flex flex-col p-8">
|
||||
<div class="w-full">
|
||||
<label>
|
||||
<span>{{ $t('IMPORT_CONTACTS.FORM.LABEL') }}</span>
|
||||
<input
|
||||
id="file"
|
||||
ref="file"
|
||||
type="file"
|
||||
accept="text/csv"
|
||||
@change="handleFileUpload"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<div class="w-full">
|
||||
<woot-button
|
||||
:disabled="uiFlags.isCreating || !file"
|
||||
:loading="uiFlags.isCreating"
|
||||
@click="uploadFile"
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('IMPORT_CONTACTS.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,42 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isCompleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('completed', this.isCompleted);
|
||||
},
|
||||
onEdit() {
|
||||
this.$emit('edit', this.id);
|
||||
},
|
||||
onDelete() {
|
||||
this.$emit('delete', this.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="reminder-wrap">
|
||||
<div class="status-wrap">
|
||||
@@ -44,45 +83,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isCompleted: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('completed', this.isCompleted);
|
||||
},
|
||||
onEdit() {
|
||||
this.$emit('edit', this.id);
|
||||
},
|
||||
onDelete() {
|
||||
this.$emit('delete', this.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.reminder-wrap {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
onClickNotes() {
|
||||
this.$emit('notes');
|
||||
},
|
||||
|
||||
onClickEvents() {
|
||||
this.$emit('events');
|
||||
},
|
||||
|
||||
onClickConversation() {
|
||||
this.$emit('conversation');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="header">
|
||||
@@ -37,24 +55,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
onClickNotes() {
|
||||
this.$emit('notes');
|
||||
},
|
||||
|
||||
onClickEvents() {
|
||||
this.$emit('events');
|
||||
},
|
||||
|
||||
onClickConversation() {
|
||||
this.$emit('conversation');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrap {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,37 +1,7 @@
|
||||
<template>
|
||||
<div class="timeline-card-wrap">
|
||||
<div class="icon-chatbox">
|
||||
<i class="ion-chatboxes" />
|
||||
</div>
|
||||
<div class="card-wrap">
|
||||
<div class="header">
|
||||
<div class="text-wrap">
|
||||
<h6 class="text-sm">
|
||||
{{ eventType }}
|
||||
</h6>
|
||||
<span class="event-path">on {{ eventPath }}</span>
|
||||
</div>
|
||||
<div class="date-wrap">
|
||||
<span>{{ readableTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-wrap">
|
||||
<p class="comment">
|
||||
{{ eventBody }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-more" @click="onClick">
|
||||
<i class="ion-android-more-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Unused file deprecated -->
|
||||
<script>
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
export default {
|
||||
mixins: [timeMixin],
|
||||
|
||||
props: {
|
||||
eventType: {
|
||||
type: String,
|
||||
@@ -53,7 +23,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
readableTime() {
|
||||
return this.dynamicTime(this.timeStamp);
|
||||
return dynamicTime(this.timeStamp);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -65,6 +35,35 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="timeline-card-wrap">
|
||||
<div class="icon-chatbox">
|
||||
<i class="ion-chatboxes" />
|
||||
</div>
|
||||
<div class="card-wrap">
|
||||
<div class="header">
|
||||
<div class="text-wrap">
|
||||
<h6 class="text-sm">
|
||||
{{ eventType }}
|
||||
</h6>
|
||||
<span class="event-path">{{ 'on' }} {{ eventPath }}</span>
|
||||
</div>
|
||||
<div class="date-wrap">
|
||||
<span>{{ readableTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-wrap">
|
||||
<p class="comment">
|
||||
{{ eventBody }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-more" @click="onClick">
|
||||
<i class="ion-android-more-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.timeline-card-wrap {
|
||||
display: flex;
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ describe('AddReminder', () => {
|
||||
});
|
||||
|
||||
it('tests resetValue', () => {
|
||||
const resetValue = jest.spyOn(wrapper.vm, 'resetValue');
|
||||
const resetValue = vi.spyOn(wrapper.vm, 'resetValue');
|
||||
wrapper.vm.content = 'test';
|
||||
wrapper.vm.date = '08/11/2022';
|
||||
wrapper.vm.resetValue();
|
||||
@@ -25,7 +25,7 @@ describe('AddReminder', () => {
|
||||
});
|
||||
|
||||
it('tests optionSelected', () => {
|
||||
const optionSelected = jest.spyOn(wrapper.vm, 'optionSelected');
|
||||
const optionSelected = vi.spyOn(wrapper.vm, 'optionSelected');
|
||||
wrapper.vm.label = '';
|
||||
wrapper.vm.optionSelected({ target: { value: 'test' } });
|
||||
expect(wrapper.vm.label).toEqual('test');
|
||||
@@ -33,7 +33,7 @@ describe('AddReminder', () => {
|
||||
});
|
||||
|
||||
it('tests onAdd', () => {
|
||||
const onAdd = jest.spyOn(wrapper.vm, 'onAdd');
|
||||
const onAdd = vi.spyOn(wrapper.vm, 'onAdd');
|
||||
wrapper.vm.label = 'label';
|
||||
wrapper.vm.content = 'content';
|
||||
wrapper.vm.date = '08/11/2022';
|
||||
|
||||
@@ -1,58 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between flex-col h-full m-0 flex-1 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<settings-header
|
||||
button-route="new"
|
||||
:header-title="contact.name"
|
||||
show-back-button
|
||||
:back-button-label="$t('CONTACT_PROFILE.BACK_BUTTON')"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="false"
|
||||
>
|
||||
<thumbnail
|
||||
v-if="contact.thumbnail"
|
||||
:src="contact.thumbnail"
|
||||
:username="contact.name"
|
||||
size="32px"
|
||||
class="mr-2 rtl:mr-0 rtl:ml-2"
|
||||
/>
|
||||
</settings-header>
|
||||
|
||||
<div v-if="uiFlags.isFetchingItem" class="text-center p-4 text-base h-full">
|
||||
<spinner size="" />
|
||||
<span>{{ $t('CONTACT_PROFILE.LOADING') }}</span>
|
||||
</div>
|
||||
<div v-else-if="contact.id" class="overflow-hidden flex-1 min-w-0">
|
||||
<div class="flex flex-wrap ml-auto mr-auto max-w-full h-full">
|
||||
<contact-info-panel
|
||||
:show-close-button="false"
|
||||
:show-avatar="false"
|
||||
:contact="contact"
|
||||
/>
|
||||
<div class="w-3/4 h-full">
|
||||
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
<div
|
||||
class="bg-slate-25 dark:bg-slate-800 h-[calc(100%-40px)] p-4 overflow-auto"
|
||||
>
|
||||
<contact-notes
|
||||
v-if="selectedTabIndex === 0"
|
||||
:contact-id="Number(contactId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactInfoPanel from '../components/ContactInfoPanel.vue';
|
||||
@@ -117,3 +62,58 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between flex-col h-full m-0 flex-1 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<SettingsHeader
|
||||
button-route="new"
|
||||
:header-title="contact.name"
|
||||
show-back-button
|
||||
:back-button-label="$t('CONTACT_PROFILE.BACK_BUTTON')"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="false"
|
||||
>
|
||||
<Thumbnail
|
||||
v-if="contact.thumbnail"
|
||||
:src="contact.thumbnail"
|
||||
:username="contact.name"
|
||||
size="32px"
|
||||
class="mr-2 rtl:mr-0 rtl:ml-2"
|
||||
/>
|
||||
</SettingsHeader>
|
||||
|
||||
<div v-if="uiFlags.isFetchingItem" class="text-center p-4 text-base h-full">
|
||||
<Spinner size="" />
|
||||
<span>{{ $t('CONTACT_PROFILE.LOADING') }}</span>
|
||||
</div>
|
||||
<div v-else-if="contact.id" class="overflow-hidden flex-1 min-w-0">
|
||||
<div class="flex flex-wrap ml-auto mr-auto max-w-full h-full">
|
||||
<ContactInfoPanel
|
||||
:show-close-button="false"
|
||||
:show-avatar="false"
|
||||
:contact="contact"
|
||||
/>
|
||||
<div class="w-3/4 h-full">
|
||||
<woot-tabs :index="selectedTabIndex" @change="onClickTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
<div
|
||||
class="bg-slate-25 dark:bg-slate-800 h-[calc(100%-40px)] p-4 overflow-auto"
|
||||
>
|
||||
<ContactNotes
|
||||
v-if="selectedTabIndex === 0"
|
||||
:contact-id="Number(contactId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -7,13 +7,17 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts'),
|
||||
name: 'contacts_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
|
||||
name: 'contacts_segments_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
return { segmentsId: route.params.id };
|
||||
@@ -22,7 +26,9 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
|
||||
name: 'contacts_labels_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
return { label: route.params.label };
|
||||
@@ -31,7 +37,9 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/:contactId'),
|
||||
name: 'contact_profile_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactManageView,
|
||||
props: route => {
|
||||
return { contactId: route.params.contactId };
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
<template>
|
||||
<div class="contact-conversation--panel">
|
||||
<div v-if="!uiFlags.isFetching" class="contact-conversation__wrap">
|
||||
<div v-if="!previousConversations.length" class="no-label-message">
|
||||
<span>
|
||||
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="contact-conversation--list">
|
||||
<conversation-card
|
||||
v-for="conversation in previousConversations"
|
||||
:key="conversation.id"
|
||||
:chat="conversation"
|
||||
:hide-inbox-name="false"
|
||||
:hide-thumbnail="true"
|
||||
class="compact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<spinner v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConversationCard from 'dashboard/components/widgets/conversation/ConversationCard.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -69,6 +46,29 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="contact-conversation--panel">
|
||||
<div v-if="!uiFlags.isFetching" class="contact-conversation__wrap">
|
||||
<div v-if="!previousConversations.length" class="no-label-message">
|
||||
<span>
|
||||
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="contact-conversation--list">
|
||||
<ConversationCard
|
||||
v-for="conversation in previousConversations"
|
||||
:key="conversation.id"
|
||||
:chat="conversation"
|
||||
:hide-inbox-name="false"
|
||||
hide-thumbnail
|
||||
class="compact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Spinner v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.no-label-message {
|
||||
@apply text-slate-500 dark:text-slate-400 mb-4;
|
||||
|
||||
+20
-20
@@ -1,23 +1,3 @@
|
||||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<div
|
||||
v-for="attribute in listOfAttributes"
|
||||
:key="attribute"
|
||||
class="custom-attribute--row"
|
||||
>
|
||||
<div class="custom-attribute--row__attribute">
|
||||
{{ attribute }}
|
||||
</div>
|
||||
<div>
|
||||
<span v-dompurify-html="valueWithLink(customAttributes[attribute])" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!listOfAttributes.length">
|
||||
{{ $t('CUSTOM_ATTRIBUTES.NOT_AVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
||||
|
||||
@@ -56,6 +36,26 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<div
|
||||
v-for="attribute in listOfAttributes"
|
||||
:key="attribute"
|
||||
class="custom-attribute--row"
|
||||
>
|
||||
<div class="custom-attribute--row__attribute">
|
||||
{{ attribute }}
|
||||
</div>
|
||||
<div>
|
||||
<span v-dompurify-html="valueWithLink(customAttributes[attribute])" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!listOfAttributes.length">
|
||||
{{ $t('CUSTOM_ATTRIBUTES.NOT_AVAILABLE') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-attributes--panel {
|
||||
margin-bottom: var(--space-normal);
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
value: { type: [String, Number], default: '' },
|
||||
compact: { type: Boolean, default: false },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-auto" :class="compact ? 'py-0 px-0' : 'py-3 px-4'">
|
||||
<div class="items-center flex justify-between mb-1.5">
|
||||
<span class="text-slate-800 font-medium dark:text-slate-100 text-sm">
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
|
||||
{{ title }}
|
||||
</span>
|
||||
<slot name="button" />
|
||||
@@ -13,15 +23,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
icon: { type: String, default: '' },
|
||||
emoji: { type: String, default: '' },
|
||||
value: { type: [String, Number], default: '' },
|
||||
compact: { type: Boolean, default: false },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,139 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-y-auto bg-white border-l dark:bg-slate-900 text-slate-900 dark:text-slate-300 border-slate-50 dark:border-slate-800/50 rtl:border-l-0 rtl:border-r contact--panel"
|
||||
>
|
||||
<contact-info
|
||||
:contact="contact"
|
||||
:channel-type="channelType"
|
||||
@toggle-panel="onPanelToggle"
|
||||
/>
|
||||
<draggable
|
||||
:list="conversationSidebarItems"
|
||||
:disabled="!dragEnabled"
|
||||
animation="200"
|
||||
class="list-group"
|
||||
ghost-class="ghost"
|
||||
handle=".drag-handle"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<transition-group>
|
||||
<div
|
||||
v-for="element in conversationSidebarItems"
|
||||
:key="element.name"
|
||||
class="bg-white dark:bg-gray-800"
|
||||
>
|
||||
<div
|
||||
v-if="element.name === 'conversation_actions'"
|
||||
class="conversation--actions"
|
||||
>
|
||||
<accordion-item
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_ACTIONS')"
|
||||
:is-open="isContactSidebarItemOpen('is_conv_actions_open')"
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_conv_actions_open', value)
|
||||
"
|
||||
>
|
||||
<conversation-action
|
||||
:conversation-id="conversationId"
|
||||
:inbox-id="inboxId"
|
||||
/>
|
||||
</accordion-item>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="element.name === 'conversation_participants'"
|
||||
class="conversation--actions"
|
||||
>
|
||||
<accordion-item
|
||||
:title="$t('CONVERSATION_PARTICIPANTS.SIDEBAR_TITLE')"
|
||||
:is-open="isContactSidebarItemOpen('is_conv_participants_open')"
|
||||
@click="
|
||||
value =>
|
||||
toggleSidebarUIState('is_conv_participants_open', value)
|
||||
"
|
||||
>
|
||||
<conversation-participant
|
||||
:conversation-id="conversationId"
|
||||
:inbox-id="inboxId"
|
||||
/>
|
||||
</accordion-item>
|
||||
</div>
|
||||
<div v-else-if="element.name === 'conversation_info'">
|
||||
<accordion-item
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_INFO')"
|
||||
:is-open="isContactSidebarItemOpen('is_conv_details_open')"
|
||||
compact
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_conv_details_open', value)
|
||||
"
|
||||
>
|
||||
<conversation-info
|
||||
:conversation-attributes="conversationAdditionalAttributes"
|
||||
:contact-attributes="contactAdditionalAttributes"
|
||||
/>
|
||||
</accordion-item>
|
||||
</div>
|
||||
<div v-else-if="element.name === 'contact_attributes'">
|
||||
<accordion-item
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||
:is-open="isContactSidebarItemOpen('is_contact_attributes_open')"
|
||||
compact
|
||||
@click="
|
||||
value =>
|
||||
toggleSidebarUIState('is_contact_attributes_open', value)
|
||||
"
|
||||
>
|
||||
<custom-attributes
|
||||
attribute-type="contact_attribute"
|
||||
attribute-class="conversation--attribute"
|
||||
class="even"
|
||||
attribute-from="conversation_contact_panel"
|
||||
:contact-id="contact.id"
|
||||
:empty-state-message="
|
||||
$t('CONVERSATION_CUSTOM_ATTRIBUTES.NO_RECORDS_FOUND')
|
||||
"
|
||||
/>
|
||||
</accordion-item>
|
||||
</div>
|
||||
<div v-else-if="element.name === 'previous_conversation'">
|
||||
<accordion-item
|
||||
v-if="contact.id"
|
||||
:title="
|
||||
$t('CONVERSATION_SIDEBAR.ACCORDION.PREVIOUS_CONVERSATION')
|
||||
"
|
||||
:is-open="isContactSidebarItemOpen('is_previous_conv_open')"
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_previous_conv_open', value)
|
||||
"
|
||||
>
|
||||
<contact-conversations
|
||||
:contact-id="contact.id"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
</accordion-item>
|
||||
</div>
|
||||
<woot-feature-toggle
|
||||
v-else-if="element.name === 'macros'"
|
||||
feature-key="macros"
|
||||
>
|
||||
<accordion-item
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.MACROS')"
|
||||
:is-open="isContactSidebarItemOpen('is_macro_open')"
|
||||
compact
|
||||
@click="value => toggleSidebarUIState('is_macro_open', value)"
|
||||
>
|
||||
<macros-list :conversation-id="conversationId" />
|
||||
</accordion-item>
|
||||
</woot-feature-toggle>
|
||||
</div>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
||||
import ContactConversations from './ContactConversations.vue';
|
||||
import ConversationAction from './ConversationAction.vue';
|
||||
@@ -143,7 +10,6 @@ import ContactInfo from './contact/ContactInfo.vue';
|
||||
import ConversationInfo from './ConversationInfo.vue';
|
||||
import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import MacrosList from './Macros/List.vue';
|
||||
export default {
|
||||
components: {
|
||||
@@ -154,10 +20,9 @@ export default {
|
||||
CustomAttributes,
|
||||
ConversationAction,
|
||||
ConversationParticipant,
|
||||
draggable,
|
||||
Draggable: draggable,
|
||||
MacrosList,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
@@ -172,6 +37,21 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
conversationSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
} = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
conversationSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragEnabled: true,
|
||||
@@ -182,8 +62,6 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
currentUser: 'getCurrentUser',
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
}),
|
||||
conversationAdditionalAttributes() {
|
||||
return this.currentConversationMetaData.additional_attributes || {};
|
||||
@@ -253,6 +131,139 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="overflow-y-auto bg-white border-l dark:bg-slate-900 text-slate-900 dark:text-slate-300 border-slate-50 dark:border-slate-800/50 rtl:border-l-0 rtl:border-r contact--panel"
|
||||
>
|
||||
<ContactInfo
|
||||
:contact="contact"
|
||||
:channel-type="channelType"
|
||||
@toggle-panel="onPanelToggle"
|
||||
/>
|
||||
<Draggable
|
||||
:list="conversationSidebarItems"
|
||||
:disabled="!dragEnabled"
|
||||
animation="200"
|
||||
class="list-group"
|
||||
ghost-class="ghost"
|
||||
handle=".drag-handle"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<transition-group>
|
||||
<div
|
||||
v-for="element in conversationSidebarItems"
|
||||
:key="element.name"
|
||||
class="bg-white dark:bg-gray-800"
|
||||
>
|
||||
<div
|
||||
v-if="element.name === 'conversation_actions'"
|
||||
class="conversation--actions"
|
||||
>
|
||||
<AccordionItem
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_ACTIONS')"
|
||||
:is-open="isContactSidebarItemOpen('is_conv_actions_open')"
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_conv_actions_open', value)
|
||||
"
|
||||
>
|
||||
<ConversationAction
|
||||
:conversation-id="conversationId"
|
||||
:inbox-id="inboxId"
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="element.name === 'conversation_participants'"
|
||||
class="conversation--actions"
|
||||
>
|
||||
<AccordionItem
|
||||
:title="$t('CONVERSATION_PARTICIPANTS.SIDEBAR_TITLE')"
|
||||
:is-open="isContactSidebarItemOpen('is_conv_participants_open')"
|
||||
@click="
|
||||
value =>
|
||||
toggleSidebarUIState('is_conv_participants_open', value)
|
||||
"
|
||||
>
|
||||
<ConversationParticipant
|
||||
:conversation-id="conversationId"
|
||||
:inbox-id="inboxId"
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<div v-else-if="element.name === 'conversation_info'">
|
||||
<AccordionItem
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_INFO')"
|
||||
:is-open="isContactSidebarItemOpen('is_conv_details_open')"
|
||||
compact
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_conv_details_open', value)
|
||||
"
|
||||
>
|
||||
<ConversationInfo
|
||||
:conversation-attributes="conversationAdditionalAttributes"
|
||||
:contact-attributes="contactAdditionalAttributes"
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<div v-else-if="element.name === 'contact_attributes'">
|
||||
<AccordionItem
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONTACT_ATTRIBUTES')"
|
||||
:is-open="isContactSidebarItemOpen('is_contact_attributes_open')"
|
||||
compact
|
||||
@click="
|
||||
value =>
|
||||
toggleSidebarUIState('is_contact_attributes_open', value)
|
||||
"
|
||||
>
|
||||
<CustomAttributes
|
||||
attribute-type="contact_attribute"
|
||||
attribute-class="conversation--attribute"
|
||||
class="even"
|
||||
attribute-from="conversation_contact_panel"
|
||||
:contact-id="contact.id"
|
||||
:empty-state-message="
|
||||
$t('CONVERSATION_CUSTOM_ATTRIBUTES.NO_RECORDS_FOUND')
|
||||
"
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<div v-else-if="element.name === 'previous_conversation'">
|
||||
<AccordionItem
|
||||
v-if="contact.id"
|
||||
:title="
|
||||
$t('CONVERSATION_SIDEBAR.ACCORDION.PREVIOUS_CONVERSATION')
|
||||
"
|
||||
:is-open="isContactSidebarItemOpen('is_previous_conv_open')"
|
||||
@click="
|
||||
value => toggleSidebarUIState('is_previous_conv_open', value)
|
||||
"
|
||||
>
|
||||
<ContactConversations
|
||||
:contact-id="contact.id"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
<woot-feature-toggle
|
||||
v-else-if="element.name === 'macros'"
|
||||
feature-key="macros"
|
||||
>
|
||||
<AccordionItem
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.MACROS')"
|
||||
:is-open="isContactSidebarItemOpen('is_macro_open')"
|
||||
compact
|
||||
@click="value => toggleSidebarUIState('is_macro_open', value)"
|
||||
>
|
||||
<MacrosList :conversation-id="conversationId" />
|
||||
</AccordionItem>
|
||||
</woot-feature-toggle>
|
||||
</div>
|
||||
</transition-group>
|
||||
</Draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.contact--profile {
|
||||
|
||||
@@ -1,88 +1,7 @@
|
||||
<!-- eslint-disable vue/v-slot-style -->
|
||||
<template>
|
||||
<div class="bg-white dark:bg-slate-900">
|
||||
<div class="multiselect-wrap--small">
|
||||
<contact-details-item
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
|
||||
>
|
||||
<template v-slot:button>
|
||||
<woot-button
|
||||
v-if="showSelfAssign"
|
||||
icon="arrow-right"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="onSelfAssign"
|
||||
>
|
||||
{{ $t('CONVERSATION_SIDEBAR.SELF_ASSIGN') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</contact-details-item>
|
||||
<multiselect-dropdown
|
||||
:options="agentsList"
|
||||
:selected-item="assignedAgent"
|
||||
:multiselector-title="$t('AGENT_MGMT.MULTI_SELECTOR.TITLE.AGENT')"
|
||||
:multiselector-placeholder="$t('AGENT_MGMT.MULTI_SELECTOR.PLACEHOLDER')"
|
||||
:no-search-result="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.NO_RESULTS.AGENT')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.AGENT')
|
||||
"
|
||||
@click="onClickAssignAgent"
|
||||
/>
|
||||
</div>
|
||||
<div class="multiselect-wrap--small">
|
||||
<contact-details-item
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.TEAM_LABEL')"
|
||||
/>
|
||||
<multiselect-dropdown
|
||||
:options="teamsList"
|
||||
:selected-item="assignedTeam"
|
||||
:multiselector-title="$t('AGENT_MGMT.MULTI_SELECTOR.TITLE.TEAM')"
|
||||
:multiselector-placeholder="$t('AGENT_MGMT.MULTI_SELECTOR.PLACEHOLDER')"
|
||||
:no-search-result="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.NO_RESULTS.TEAM')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.INPUT')
|
||||
"
|
||||
@click="onClickAssignTeam"
|
||||
/>
|
||||
</div>
|
||||
<div class="multiselect-wrap--small">
|
||||
<contact-details-item
|
||||
compact
|
||||
:title="$t('CONVERSATION.PRIORITY.TITLE')"
|
||||
/>
|
||||
<multiselect-dropdown
|
||||
:options="priorityOptions"
|
||||
:selected-item="assignedPriority"
|
||||
:multiselector-title="$t('CONVERSATION.PRIORITY.TITLE')"
|
||||
:multiselector-placeholder="
|
||||
$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SELECT_PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.NO_RESULTS')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.INPUT_PLACEHOLDER')
|
||||
"
|
||||
@click="onClickAssignPriority"
|
||||
/>
|
||||
</div>
|
||||
<contact-details-item
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_LABELS')"
|
||||
/>
|
||||
<conversation-labels :conversation-id="conversationId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import ConversationLabels from './labels/LabelBox.vue';
|
||||
@@ -97,12 +16,15 @@ export default {
|
||||
MultiselectDropdown,
|
||||
ConversationLabels,
|
||||
},
|
||||
mixins: [agentMixin, alertMixin, teamMixin],
|
||||
mixins: [agentMixin, teamMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
// inboxId prop is used in /mixins/agentMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
inboxId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
@@ -157,7 +79,7 @@ export default {
|
||||
agentId,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -172,7 +94,7 @@ export default {
|
||||
this.$store
|
||||
.dispatch('assignTeam', { conversationId, teamId })
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_TEAM'));
|
||||
useAlert(this.$t('CONVERSATION.CHANGE_TEAM'));
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -201,7 +123,7 @@ export default {
|
||||
newValue: priority,
|
||||
from: 'Conversation Sidebar',
|
||||
});
|
||||
this.showAlert(
|
||||
useAlert(
|
||||
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
|
||||
priority: priorityItem.name,
|
||||
conversationId,
|
||||
@@ -270,3 +192,81 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white dark:bg-slate-900">
|
||||
<div class="multiselect-wrap--small">
|
||||
<ContactDetailsItem
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
|
||||
>
|
||||
<template #button>
|
||||
<woot-button
|
||||
v-if="showSelfAssign"
|
||||
icon="arrow-right"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="onSelfAssign"
|
||||
>
|
||||
{{ $t('CONVERSATION_SIDEBAR.SELF_ASSIGN') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</ContactDetailsItem>
|
||||
<MultiselectDropdown
|
||||
:options="agentsList"
|
||||
:selected-item="assignedAgent"
|
||||
:multiselector-title="$t('AGENT_MGMT.MULTI_SELECTOR.TITLE.AGENT')"
|
||||
:multiselector-placeholder="$t('AGENT_MGMT.MULTI_SELECTOR.PLACEHOLDER')"
|
||||
:no-search-result="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.NO_RESULTS.AGENT')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.AGENT')
|
||||
"
|
||||
@click="onClickAssignAgent"
|
||||
/>
|
||||
</div>
|
||||
<div class="multiselect-wrap--small">
|
||||
<ContactDetailsItem
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.TEAM_LABEL')"
|
||||
/>
|
||||
<MultiselectDropdown
|
||||
:options="teamsList"
|
||||
:selected-item="assignedTeam"
|
||||
:multiselector-title="$t('AGENT_MGMT.MULTI_SELECTOR.TITLE.TEAM')"
|
||||
:multiselector-placeholder="$t('AGENT_MGMT.MULTI_SELECTOR.PLACEHOLDER')"
|
||||
:no-search-result="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.NO_RESULTS.TEAM')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('AGENT_MGMT.MULTI_SELECTOR.SEARCH.PLACEHOLDER.INPUT')
|
||||
"
|
||||
@click="onClickAssignTeam"
|
||||
/>
|
||||
</div>
|
||||
<div class="multiselect-wrap--small">
|
||||
<ContactDetailsItem compact :title="$t('CONVERSATION.PRIORITY.TITLE')" />
|
||||
<MultiselectDropdown
|
||||
:options="priorityOptions"
|
||||
:selected-item="assignedPriority"
|
||||
:multiselector-title="$t('CONVERSATION.PRIORITY.TITLE')"
|
||||
:multiselector-placeholder="
|
||||
$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SELECT_PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.NO_RESULTS')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.INPUT_PLACEHOLDER')
|
||||
"
|
||||
@click="onClickAssignPriority"
|
||||
/>
|
||||
</div>
|
||||
<ContactDetailsItem
|
||||
compact
|
||||
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_LABELS')"
|
||||
/>
|
||||
<ConversationLabels :conversation-id="conversationId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -99,6 +99,7 @@ const staticElements = computed(() =>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversation--attribute {
|
||||
@apply border-slate-50 dark:border-slate-700/50 border-b border-solid;
|
||||
|
||||
+86
-82
@@ -1,85 +1,6 @@
|
||||
<template>
|
||||
<div class="relative bg-white dark:bg-slate-900">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex justify-between w-full mb-1">
|
||||
<div>
|
||||
<p v-if="watchersList.length" class="total-watchers m-0 text-sm">
|
||||
<spinner v-if="watchersUiFlas.isFetching" size="tiny" />
|
||||
{{ totalWatchersText }}
|
||||
</p>
|
||||
<p v-else class="text-slate-400 dark:text-slate-700 m-0 text-sm">
|
||||
{{ $t('CONVERSATION_PARTICIPANTS.NO_PARTICIPANTS_TEXT') }}
|
||||
</p>
|
||||
</div>
|
||||
<woot-button
|
||||
v-tooltip.left="$t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS')"
|
||||
:title="$t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS')"
|
||||
icon="settings"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
@click="onOpenDropdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<thumbnail-group
|
||||
:more-thumbnails-text="moreThumbnailsText"
|
||||
:show-more-thumbnails-count="showMoreThumbs"
|
||||
:users-list="thumbnailList"
|
||||
/>
|
||||
<p
|
||||
v-if="isUserWatching"
|
||||
class="text-slate-300 dark:text-slate-300 m-0 text-sm"
|
||||
>
|
||||
{{ $t('CONVERSATION_PARTICIPANTS.YOU_ARE_WATCHING') }}
|
||||
</p>
|
||||
<woot-button
|
||||
v-else
|
||||
icon="arrow-right"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="onSelfAssign"
|
||||
>
|
||||
{{ $t('CONVERSATION_PARTICIPANTS.WATCH_CONVERSATION') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div
|
||||
v-on-clickaway="
|
||||
() => {
|
||||
onCloseDropdown();
|
||||
}
|
||||
"
|
||||
:class="{ 'dropdown-pane--open': showDropDown }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<h4
|
||||
class="text-sm m-0 overflow-hidden whitespace-nowrap text-ellipsis text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS') }}
|
||||
</h4>
|
||||
<woot-button
|
||||
icon="dismiss"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
variant="clear"
|
||||
@click="onCloseDropdown"
|
||||
/>
|
||||
</div>
|
||||
<multiselect-dropdown-items
|
||||
:options="agentsList"
|
||||
:selected-items="selectedWatchers"
|
||||
:has-thumbnail="true"
|
||||
@click="onClickItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { mapGetters } from 'vuex';
|
||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||
import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup.vue';
|
||||
@@ -91,12 +12,15 @@ export default {
|
||||
ThumbnailGroup,
|
||||
MultiselectDropdownItems,
|
||||
},
|
||||
mixins: [alertMixin, agentMixin],
|
||||
mixins: [agentMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
// inboxId prop is used in /mixins/agentMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
inboxId: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
@@ -197,7 +121,7 @@ export default {
|
||||
error?.message ||
|
||||
this.$t('CONVERSATION_PARTICIPANTS.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(alertMessage);
|
||||
useAlert(alertMessage);
|
||||
}
|
||||
this.fetchParticipants();
|
||||
},
|
||||
@@ -228,6 +152,86 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative bg-white dark:bg-slate-900">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex justify-between w-full mb-1">
|
||||
<div>
|
||||
<p v-if="watchersList.length" class="m-0 text-sm total-watchers">
|
||||
<Spinner v-if="watchersUiFlas.isFetching" size="tiny" />
|
||||
{{ totalWatchersText }}
|
||||
</p>
|
||||
<p v-else class="m-0 text-sm text-slate-400 dark:text-slate-700">
|
||||
{{ $t('CONVERSATION_PARTICIPANTS.NO_PARTICIPANTS_TEXT') }}
|
||||
</p>
|
||||
</div>
|
||||
<woot-button
|
||||
v-tooltip.left="$t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS')"
|
||||
:title="$t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS')"
|
||||
icon="settings"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
@click="onOpenDropdown"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<ThumbnailGroup
|
||||
:more-thumbnails-text="moreThumbnailsText"
|
||||
:show-more-thumbnails-count="showMoreThumbs"
|
||||
:users-list="thumbnailList"
|
||||
/>
|
||||
<p
|
||||
v-if="isUserWatching"
|
||||
class="m-0 text-sm text-slate-300 dark:text-slate-300"
|
||||
>
|
||||
{{ $t('CONVERSATION_PARTICIPANTS.YOU_ARE_WATCHING') }}
|
||||
</p>
|
||||
<woot-button
|
||||
v-else
|
||||
icon="arrow-right"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="onSelfAssign"
|
||||
>
|
||||
{{ $t('CONVERSATION_PARTICIPANTS.WATCH_CONVERSATION') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div
|
||||
v-on-clickaway="
|
||||
() => {
|
||||
onCloseDropdown();
|
||||
}
|
||||
"
|
||||
:class="{ 'dropdown-pane--open': showDropDown }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h4
|
||||
class="m-0 overflow-hidden text-sm whitespace-nowrap text-ellipsis text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS') }}
|
||||
</h4>
|
||||
<woot-button
|
||||
icon="dismiss"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
variant="clear"
|
||||
@click="onCloseDropdown"
|
||||
/>
|
||||
</div>
|
||||
<MultiselectDropdownItems
|
||||
:options="agentsList"
|
||||
:selected-items="selectedWatchers"
|
||||
has-thumbnail
|
||||
@click="onClickItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-pane {
|
||||
@apply box-border top-8 w-full;
|
||||
|
||||
@@ -1,46 +1,20 @@
|
||||
<template>
|
||||
<section class="conversation-page bg-white dark:bg-slate-900">
|
||||
<chat-list
|
||||
:show-conversation-list="showConversationList"
|
||||
:conversation-inbox="inboxId"
|
||||
:label="label"
|
||||
:team-id="teamId"
|
||||
:conversation-type="conversationType"
|
||||
:folders-id="foldersId"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@conversation-load="onConversationLoad"
|
||||
>
|
||||
<pop-over-search
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggle-conversation-layout="toggleConversationLayout"
|
||||
/>
|
||||
</chat-list>
|
||||
<conversation-box
|
||||
v-if="showMessageView"
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import ChatList from '../../../components/ChatList.vue';
|
||||
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
|
||||
import PopOverSearch from './search/PopOverSearch.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatList,
|
||||
ConversationBox,
|
||||
PopOverSearch,
|
||||
CmdBarConversationSnooze,
|
||||
},
|
||||
mixins: [uiSettingsMixin],
|
||||
props: {
|
||||
inboxId: {
|
||||
type: [String, Number],
|
||||
@@ -67,6 +41,14 @@ export default {
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSearchModal: false,
|
||||
@@ -189,6 +171,35 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-white conversation-page dark:bg-slate-900">
|
||||
<ChatList
|
||||
:show-conversation-list="showConversationList"
|
||||
:conversation-inbox="inboxId"
|
||||
:label="label"
|
||||
:team-id="teamId"
|
||||
:conversation-type="conversationType"
|
||||
:folders-id="foldersId"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@conversationLoad="onConversationLoad"
|
||||
>
|
||||
<PopOverSearch
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggleConversationLayout="toggleConversationLayout"
|
||||
/>
|
||||
</ChatList>
|
||||
<ConversationBox
|
||||
v-if="showMessageView"
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@contactPanelToggle="onToggleContactPanel"
|
||||
/>
|
||||
<CmdBarConversationSnooze />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.conversation-page {
|
||||
display: flex;
|
||||
|
||||
@@ -1,37 +1,3 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="!uiFlags.isFetching && !macros.length"
|
||||
class="macros_list--empty-state"
|
||||
>
|
||||
<p class="flex h-full items-center flex-col justify-center">
|
||||
{{ $t('MACROS.LIST.404') }}
|
||||
</p>
|
||||
<router-link :to="addAccountScoping('settings/macros')">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
icon="add"
|
||||
size="tiny"
|
||||
class="macros_add-button"
|
||||
>
|
||||
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('MACROS.LOADING')"
|
||||
/>
|
||||
<div v-if="!uiFlags.isFetching && macros.length" class="macros-list">
|
||||
<macro-item
|
||||
v-for="macro in macros"
|
||||
:key="macro.id"
|
||||
:macro="macro"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MacroItem from './MacroItem.vue';
|
||||
@@ -59,6 +25,42 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="!uiFlags.isFetching && !macros.length"
|
||||
class="macros_list--empty-state"
|
||||
>
|
||||
<p class="flex h-full items-center flex-col justify-center">
|
||||
{{ $t('MACROS.LIST.404') }}
|
||||
</p>
|
||||
<router-link :to="addAccountScoping('settings/macros')">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
icon="add"
|
||||
size="tiny"
|
||||
class="macros_add-button"
|
||||
>
|
||||
{{ $t('MACROS.HEADER_BTN_TXT') }}
|
||||
</woot-button>
|
||||
</router-link>
|
||||
</div>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('MACROS.LOADING')"
|
||||
/>
|
||||
<div v-if="!uiFlags.isFetching && macros.length" class="macros-list">
|
||||
<MacroItem
|
||||
v-for="macro in macros"
|
||||
:key="macro.id"
|
||||
:macro="macro"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macros-list {
|
||||
padding: var(--space-smaller);
|
||||
|
||||
@@ -1,39 +1,5 @@
|
||||
<template>
|
||||
<div class="macro button secondary clear">
|
||||
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
|
||||
macro.name
|
||||
}}</span>
|
||||
<div class="macros-actions flex items-center gap-1">
|
||||
<woot-button
|
||||
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="info"
|
||||
@click="toggleMacroPreview(macro)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.left-start="$t('MACROS.EXECUTE.BUTTON_TOOLTIP')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="play-circle"
|
||||
:is-loading="isExecuting"
|
||||
@click="executeMacro(macro)"
|
||||
/>
|
||||
</div>
|
||||
<transition name="menu-slide">
|
||||
<macro-preview
|
||||
v-if="showPreview"
|
||||
v-on-clickaway="closeMacroPreview"
|
||||
:macro="macro"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import MacroPreview from './MacroPreview.vue';
|
||||
import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -41,7 +7,6 @@ export default {
|
||||
components: {
|
||||
MacroPreview,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
macro: {
|
||||
type: Object,
|
||||
@@ -67,9 +32,9 @@ export default {
|
||||
conversationIds: [this.conversationId],
|
||||
});
|
||||
this.$track(CONVERSATION_EVENTS.EXECUTED_A_MACRO);
|
||||
this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
|
||||
useAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('MACROS.ERROR'));
|
||||
useAlert(this.$t('MACROS.ERROR'));
|
||||
} finally {
|
||||
this.isExecuting = false;
|
||||
}
|
||||
@@ -84,6 +49,40 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="macro button secondary clear">
|
||||
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
|
||||
macro.name
|
||||
}}</span>
|
||||
<div class="flex items-center gap-1 macros-actions">
|
||||
<woot-button
|
||||
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="info"
|
||||
@click="toggleMacroPreview(macro)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.left-start="$t('MACROS.EXECUTE.BUTTON_TOOLTIP')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="play-circle"
|
||||
:is-loading="isExecuting"
|
||||
@click="executeMacro(macro)"
|
||||
/>
|
||||
</div>
|
||||
<transition name="menu-slide">
|
||||
<MacroPreview
|
||||
v-if="showPreview"
|
||||
v-on-clickaway="closeMacroPreview"
|
||||
:macro="macro"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macro {
|
||||
@apply relative flex items-center justify-between leading-4;
|
||||
|
||||
@@ -1,30 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="macro-preview absolute max-h-[22.5rem] w-64 rounded-md bg-white dark:bg-slate-800 shadow-lg bottom-8 right-8 overflow-y-auto p-4 text-left rtl:text-right"
|
||||
>
|
||||
<h6 class="text-sm text-slate-800 dark:text-slate-100 mb-4">
|
||||
{{ macro.name }}
|
||||
</h6>
|
||||
<div
|
||||
v-for="(action, i) in resolvedMacro"
|
||||
:key="i"
|
||||
class="relative pl-4 macro-block"
|
||||
>
|
||||
<div
|
||||
v-if="i !== macro.actions.length - 1"
|
||||
class="top-[0.390625rem] absolute -bottom-1 left-0 w-px bg-slate-75 dark:bg-slate-600"
|
||||
/>
|
||||
<div
|
||||
class="absolute -left-[0.21875rem] top-[0.2734375rem] w-2 h-2 rounded-full bg-white dark:bg-slate-200 border-2 border-solid border-slate-100 dark:border-slate-600"
|
||||
/>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mb-1">
|
||||
{{ action.actionName }}
|
||||
</p>
|
||||
<p class="text-slate-800 dark:text-slate-100">{{ action.actionValue }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
resolveActionName,
|
||||
@@ -81,6 +54,33 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="macro-preview absolute max-h-[22.5rem] w-64 rounded-md bg-white dark:bg-slate-800 shadow-lg bottom-8 right-8 overflow-y-auto p-4 text-left rtl:text-right"
|
||||
>
|
||||
<h6 class="text-sm text-slate-800 dark:text-slate-100 mb-4">
|
||||
{{ macro.name }}
|
||||
</h6>
|
||||
<div
|
||||
v-for="(action, i) in resolvedMacro"
|
||||
:key="i"
|
||||
class="relative pl-4 macro-block"
|
||||
>
|
||||
<div
|
||||
v-if="i !== macro.actions.length - 1"
|
||||
class="top-[0.390625rem] absolute -bottom-1 left-0 w-px bg-slate-75 dark:bg-slate-600"
|
||||
/>
|
||||
<div
|
||||
class="absolute -left-[0.21875rem] top-[0.2734375rem] w-2 h-2 rounded-full bg-white dark:bg-slate-200 border-2 border-solid border-slate-100 dark:border-slate-600"
|
||||
/>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mb-1">
|
||||
{{ action.actionName }}
|
||||
</p>
|
||||
<p class="text-slate-800 dark:text-slate-100">{{ action.actionValue }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.macro-preview {
|
||||
.macro-block {
|
||||
|
||||
@@ -1,167 +1,16 @@
|
||||
<template>
|
||||
<form
|
||||
class="w-full px-8 pt-6 pb-8 contact--form"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<woot-avatar-uploader
|
||||
:label="$t('CONTACT_FORM.FORM.AVATAR.LABEL')"
|
||||
:src="avatarUrl"
|
||||
:username-avatar="name"
|
||||
:delete-avatar="!!avatarUrl"
|
||||
class="settings-item"
|
||||
@change="handleImageUpload"
|
||||
@onAvatarDelete="handleAvatarDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: $v.name.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="name"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.NAME.PLACEHOLDER')"
|
||||
@input="$v.name.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: $v.email.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.LABEL') }}
|
||||
<input
|
||||
v-model.trim="email"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.PLACEHOLDER')"
|
||||
@input="$v.email.$touch"
|
||||
/>
|
||||
<span v-if="$v.email.$error" class="message">
|
||||
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: $v.description.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.BIO.LABEL') }}
|
||||
<textarea
|
||||
v-model.trim="description"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.BIO.PLACEHOLDER')"
|
||||
@input="$v.description.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label
|
||||
:class="{
|
||||
error: isPhoneNumberNotValid,
|
||||
}"
|
||||
>
|
||||
{{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.LABEL') }}
|
||||
<woot-phone-input
|
||||
v-model="phoneNumber"
|
||||
:value="phoneNumber"
|
||||
:error="isPhoneNumberNotValid"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.PHONE_NUMBER.PLACEHOLDER')"
|
||||
@input="onPhoneNumberInputChange"
|
||||
@blur="$v.phoneNumber.$touch"
|
||||
@setCode="setPhoneCode"
|
||||
/>
|
||||
<span v-if="isPhoneNumberNotValid" class="message">
|
||||
{{ phoneNumberError }}
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
v-if="isPhoneNumberNotValid || !phoneNumber"
|
||||
class="relative mx-0 mt-0 mb-2.5 p-2 rounded-md text-sm border border-solid border-yellow-500 text-yellow-700 dark:border-yellow-700 bg-yellow-200/60 dark:bg-yellow-200/20 dark:text-yellow-400"
|
||||
>
|
||||
{{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.HELP') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model.trim="companyName"
|
||||
class="w-full"
|
||||
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
|
||||
/>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label>
|
||||
{{ $t('CONTACT_FORM.FORM.COUNTRY.LABEL') }}
|
||||
</label>
|
||||
<multiselect
|
||||
v-model="country"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.COUNTRY.PLACEHOLDER')"
|
||||
selected-label
|
||||
:select-label="$t('CONTACT_FORM.FORM.COUNTRY.SELECT_PLACEHOLDER')"
|
||||
:deselect-label="$t('CONTACT_FORM.FORM.COUNTRY.REMOVE')"
|
||||
:custom-label="countryNameWithCode"
|
||||
:max-height="160"
|
||||
:options="countries"
|
||||
:allow-empty="true"
|
||||
:option-height="104"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model="city"
|
||||
class="w-full"
|
||||
:label="$t('CONTACT_FORM.FORM.CITY.LABEL')"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.CITY.PLACEHOLDER')"
|
||||
/>
|
||||
|
||||
<div class="w-full">
|
||||
<label> Social Profiles </label>
|
||||
<div
|
||||
v-for="socialProfile in socialProfileKeys"
|
||||
:key="socialProfile.key"
|
||||
class="flex items-stretch w-full mb-4"
|
||||
>
|
||||
<span
|
||||
class="flex items-center h-10 px-2 text-sm border-solid bg-slate-50 border-y ltr:border-l rtl:border-r ltr:rounded-l-md rtl:rounded-r-md dark:bg-slate-700 text-slate-800 dark:text-slate-100 border-slate-200 dark:border-slate-600"
|
||||
>
|
||||
{{ socialProfile.prefixURL }}
|
||||
</span>
|
||||
<input
|
||||
v-model="socialProfileUserNames[socialProfile.key]"
|
||||
class="input-group-field ltr:rounded-l-none rtl:rounded-r-none !mb-0"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<div class="w-full">
|
||||
<woot-submit-button
|
||||
:loading="inProgress"
|
||||
:button-text="$t('CONTACT_FORM.FORM.SUBMIT')"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onCancel">
|
||||
{{ $t('CONTACT_FORM.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import {
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
import { required, email } from 'vuelidate/lib/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, email } from '@vuelidate/validators';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { isPhoneNumberValid } from 'shared/helpers/Validators';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
@@ -176,6 +25,9 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
countries: countries,
|
||||
@@ -368,27 +220,25 @@ export default {
|
||||
}
|
||||
},
|
||||
async handleSubmit() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid || this.isPhoneNumberNotValid) {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid || this.isPhoneNumberNotValid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.onSubmit(this.getContactObject());
|
||||
this.onSuccess();
|
||||
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
if (error.data.includes('email')) {
|
||||
this.showAlert(
|
||||
this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE')
|
||||
);
|
||||
useAlert(this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE'));
|
||||
} else if (error.data.includes('phone_number')) {
|
||||
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
this.showAlert(error.data);
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -400,15 +250,13 @@ export default {
|
||||
try {
|
||||
if (this.contact && this.contact.id) {
|
||||
await this.$store.dispatch('contacts/deleteAvatar', this.contact.id);
|
||||
this.showAlert(
|
||||
this.$t('CONTACT_FORM.DELETE_AVATAR.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
useAlert(this.$t('CONTACT_FORM.DELETE_AVATAR.API.SUCCESS_MESSAGE'));
|
||||
}
|
||||
this.avatarFile = null;
|
||||
this.avatarUrl = '';
|
||||
this.activeDialCode = '';
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
useAlert(
|
||||
error.message
|
||||
? error.message
|
||||
: this.$t('CONTACT_FORM.DELETE_AVATAR.API.ERROR_MESSAGE')
|
||||
@@ -419,6 +267,157 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="w-full px-8 pt-6 pb-8 contact--form"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<woot-avatar-uploader
|
||||
:label="$t('CONTACT_FORM.FORM.AVATAR.LABEL')"
|
||||
:src="avatarUrl"
|
||||
:username-avatar="name"
|
||||
:delete-avatar="!!avatarUrl"
|
||||
class="settings-item"
|
||||
@change="handleImageUpload"
|
||||
@onAvatarDelete="handleAvatarDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="name"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.NAME.PLACEHOLDER')"
|
||||
@input="v$.name.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label :class="{ error: v$.email.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.LABEL') }}
|
||||
<input
|
||||
v-model.trim="email"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.PLACEHOLDER')"
|
||||
@input="v$.email.$touch"
|
||||
/>
|
||||
<span v-if="v$.email.$error" class="message">
|
||||
{{ $t('CONTACT_FORM.FORM.EMAIL_ADDRESS.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.description.$error }">
|
||||
{{ $t('CONTACT_FORM.FORM.BIO.LABEL') }}
|
||||
<textarea
|
||||
v-model.trim="description"
|
||||
type="text"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.BIO.PLACEHOLDER')"
|
||||
@input="v$.description.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label
|
||||
:class="{
|
||||
error: isPhoneNumberNotValid,
|
||||
}"
|
||||
>
|
||||
{{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.LABEL') }}
|
||||
<woot-phone-input
|
||||
v-model="phoneNumber"
|
||||
:value="phoneNumber"
|
||||
:error="isPhoneNumberNotValid"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.PHONE_NUMBER.PLACEHOLDER')"
|
||||
@input="onPhoneNumberInputChange"
|
||||
@blur="v$.phoneNumber.$touch"
|
||||
@setCode="setPhoneCode"
|
||||
/>
|
||||
<span v-if="isPhoneNumberNotValid" class="message">
|
||||
{{ phoneNumberError }}
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
v-if="isPhoneNumberNotValid || !phoneNumber"
|
||||
class="relative mx-0 mt-0 mb-2.5 p-2 rounded-md text-sm border border-solid border-yellow-500 text-yellow-700 dark:border-yellow-700 bg-yellow-200/60 dark:bg-yellow-200/20 dark:text-yellow-400"
|
||||
>
|
||||
{{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.HELP') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model.trim="companyName"
|
||||
class="w-full"
|
||||
:label="$t('CONTACT_FORM.FORM.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.COMPANY_NAME.PLACEHOLDER')"
|
||||
/>
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<label>
|
||||
{{ $t('CONTACT_FORM.FORM.COUNTRY.LABEL') }}
|
||||
</label>
|
||||
<multiselect
|
||||
v-model="country"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.COUNTRY.PLACEHOLDER')"
|
||||
selected-label
|
||||
:select-label="$t('CONTACT_FORM.FORM.COUNTRY.SELECT_PLACEHOLDER')"
|
||||
:deselect-label="$t('CONTACT_FORM.FORM.COUNTRY.REMOVE')"
|
||||
:custom-label="countryNameWithCode"
|
||||
:max-height="160"
|
||||
:options="countries"
|
||||
allow-empty
|
||||
:option-height="104"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model="city"
|
||||
class="w-full"
|
||||
:label="$t('CONTACT_FORM.FORM.CITY.LABEL')"
|
||||
:placeholder="$t('CONTACT_FORM.FORM.CITY.PLACEHOLDER')"
|
||||
/>
|
||||
|
||||
<div class="w-full">
|
||||
<label>{{ $t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES') }}</label>
|
||||
<div
|
||||
v-for="socialProfile in socialProfileKeys"
|
||||
:key="socialProfile.key"
|
||||
class="flex items-stretch w-full mb-4"
|
||||
>
|
||||
<span
|
||||
class="flex items-center h-10 px-2 text-sm border-solid bg-slate-50 border-y ltr:border-l rtl:border-r ltr:rounded-l-md rtl:rounded-r-md dark:bg-slate-700 text-slate-800 dark:text-slate-100 border-slate-200 dark:border-slate-600"
|
||||
>
|
||||
{{ socialProfile.prefixURL }}
|
||||
</span>
|
||||
<input
|
||||
v-model="socialProfileUserNames[socialProfile.key]"
|
||||
class="input-group-field ltr:!rounded-l-none rtl:rounded-r-none !mb-0"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<div class="w-full">
|
||||
<woot-submit-button
|
||||
:loading="inProgress"
|
||||
:button-text="$t('CONTACT_FORM.FORM.SUBMIT')"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onCancel">
|
||||
{{ $t('CONTACT_FORM.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
::v-deep {
|
||||
.multiselect .multiselect__tags .multiselect__single {
|
||||
|
||||
@@ -1,182 +1,14 @@
|
||||
<template>
|
||||
<div class="relative items-center p-4 bg-white dark:bg-slate-900 w-full">
|
||||
<div class="text-left rtl:text-right flex flex-col gap-2 w-full">
|
||||
<div class="flex justify-between flex-row">
|
||||
<thumbnail
|
||||
v-if="showAvatar"
|
||||
:src="contact.thumbnail"
|
||||
size="56px"
|
||||
:username="contact.name"
|
||||
:status="contact.availability_status"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showCloseButton"
|
||||
:icon="closeIconName"
|
||||
class="clear secondary rtl:rotate-180"
|
||||
@click="onPanelToggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-start gap-1.5 min-w-0 w-full">
|
||||
<div v-if="showAvatar" class="flex items-start gap-2 min-w-0 w-full">
|
||||
<h3
|
||||
class="flex-shrink min-w-0 text-base text-slate-800 dark:text-slate-100 capitalize my-0 max-w-full break-words"
|
||||
>
|
||||
{{ contact.name }}
|
||||
</h3>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<fluent-icon
|
||||
v-if="contact.created_at"
|
||||
v-tooltip.left="
|
||||
`${$t('CONTACT_PANEL.CREATED_AT_LABEL')} ${dynamicTime(
|
||||
contact.created_at
|
||||
)}`
|
||||
"
|
||||
icon="info"
|
||||
size="14"
|
||||
class="mt-0.5"
|
||||
/>
|
||||
<a
|
||||
:href="contactProfileLink"
|
||||
class="text-base"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
>
|
||||
<woot-button
|
||||
size="tiny"
|
||||
icon="open"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="additionalAttributes.description" class="break-words mb-0.5">
|
||||
{{ additionalAttributes.description }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 items-start w-full">
|
||||
<contact-info-row
|
||||
:href="contact.email ? `mailto:${contact.email}` : ''"
|
||||
:value="contact.email"
|
||||
icon="mail"
|
||||
emoji="✉️"
|
||||
:title="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
||||
show-copy
|
||||
/>
|
||||
<contact-info-row
|
||||
:href="contact.phone_number ? `tel:${contact.phone_number}` : ''"
|
||||
:value="contact.phone_number"
|
||||
icon="call"
|
||||
emoji="📞"
|
||||
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
show-copy
|
||||
/>
|
||||
<contact-info-row
|
||||
v-if="contact.identifier"
|
||||
:value="contact.identifier"
|
||||
icon="contact-identify"
|
||||
emoji="🪪"
|
||||
:title="$t('CONTACT_PANEL.IDENTIFIER')"
|
||||
/>
|
||||
<contact-info-row
|
||||
:value="additionalAttributes.company_name"
|
||||
icon="building-bank"
|
||||
emoji="🏢"
|
||||
:title="$t('CONTACT_PANEL.COMPANY')"
|
||||
/>
|
||||
<contact-info-row
|
||||
v-if="location || additionalAttributes.location"
|
||||
:value="location || additionalAttributes.location"
|
||||
icon="map"
|
||||
emoji="🌍"
|
||||
:title="$t('CONTACT_PANEL.LOCATION')"
|
||||
/>
|
||||
<social-icons :social-profiles="socialProfiles" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full mt-0.5 gap-2">
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
title="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
icon="chat"
|
||||
size="small"
|
||||
@click="toggleConversationModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
title="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
icon="edit"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="toggleEditModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.MERGE_CONTACT')"
|
||||
title="$t('CONTACT_PANEL.MERGE_CONTACT')"
|
||||
icon="merge"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
:disabled="uiFlags.isMerging"
|
||||
@click="openMergeModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
v-tooltip="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
title="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
icon="delete"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="alert"
|
||||
:disabled="uiFlags.isDeleting"
|
||||
@click="toggleDeleteModal"
|
||||
/>
|
||||
</div>
|
||||
<edit-contact
|
||||
v-if="showEditModal"
|
||||
:show="showEditModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleEditModal"
|
||||
/>
|
||||
<new-conversation
|
||||
v-if="contact.id"
|
||||
:show="showConversationModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleConversationModal"
|
||||
/>
|
||||
<contact-merge-modal
|
||||
v-if="showMergeModal"
|
||||
:primary-contact="contact"
|
||||
:show="showMergeModal"
|
||||
@close="toggleMergeModal"
|
||||
/>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
v-if="showDeleteModal"
|
||||
:show.sync="showDeleteModal"
|
||||
:on-close="closeDelete"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('DELETE_CONTACT.CONFIRM.TITLE')"
|
||||
:message="$t('DELETE_CONTACT.CONFIRM.MESSAGE')"
|
||||
:message-value="confirmDeleteMessage"
|
||||
:confirm-text="$t('DELETE_CONTACT.CONFIRM.YES')"
|
||||
:reject-text="$t('DELETE_CONTACT.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import ContactInfoRow from './ContactInfoRow.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import SocialIcons from './SocialIcons.vue';
|
||||
|
||||
import EditContact from './EditContact.vue';
|
||||
import NewConversation from './NewConversation.vue';
|
||||
import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import adminMixin from '../../../../mixins/isAdmin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
@@ -194,16 +26,11 @@ export default {
|
||||
NewConversation,
|
||||
ContactMergeModal,
|
||||
},
|
||||
mixins: [alertMixin, adminMixin, timeMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
channelType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@@ -217,6 +44,12 @@ export default {
|
||||
default: 'chevron-right',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showEditModal: false,
|
||||
@@ -260,6 +93,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
toggleMergeModal() {
|
||||
this.showMergeModal = !this.showMergeModal;
|
||||
},
|
||||
@@ -267,7 +101,7 @@ export default {
|
||||
this.showEditModal = !this.showEditModal;
|
||||
},
|
||||
onPanelToggle() {
|
||||
this.$emit('toggle-panel');
|
||||
this.$emit('togglePanel');
|
||||
},
|
||||
toggleConversationModal() {
|
||||
this.showConversationModal = !this.showConversationModal;
|
||||
@@ -299,8 +133,8 @@ export default {
|
||||
async deleteContact({ id }) {
|
||||
try {
|
||||
await this.$store.dispatch('contacts/delete', id);
|
||||
this.$emit('panel-close');
|
||||
this.showAlert(this.$t('DELETE_CONTACT.API.SUCCESS_MESSAGE'));
|
||||
this.$emit('panelClose');
|
||||
useAlert(this.$t('DELETE_CONTACT.API.SUCCESS_MESSAGE'));
|
||||
|
||||
if (isAConversationRoute(this.$route.name)) {
|
||||
this.$router.push({
|
||||
@@ -316,7 +150,7 @@ export default {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
useAlert(
|
||||
error.message
|
||||
? error.message
|
||||
: this.$t('DELETE_CONTACT.API.ERROR_MESSAGE')
|
||||
@@ -329,3 +163,171 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative items-center w-full p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-col w-full gap-2 text-left rtl:text-right">
|
||||
<div class="flex flex-row justify-between">
|
||||
<Thumbnail
|
||||
v-if="showAvatar"
|
||||
:src="contact.thumbnail"
|
||||
size="56px"
|
||||
:username="contact.name"
|
||||
:status="contact.availability_status"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showCloseButton"
|
||||
:icon="closeIconName"
|
||||
class="clear secondary rtl:rotate-180"
|
||||
@click="onPanelToggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-start gap-1.5 min-w-0 w-full">
|
||||
<div v-if="showAvatar" class="flex items-start w-full min-w-0 gap-2">
|
||||
<h3
|
||||
class="flex-shrink max-w-full min-w-0 my-0 text-base capitalize break-words text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ contact.name }}
|
||||
</h3>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<fluent-icon
|
||||
v-if="contact.created_at"
|
||||
v-tooltip.left="
|
||||
`${$t('CONTACT_PANEL.CREATED_AT_LABEL')} ${dynamicTime(
|
||||
contact.created_at
|
||||
)}`
|
||||
"
|
||||
icon="info"
|
||||
size="14"
|
||||
class="mt-0.5"
|
||||
/>
|
||||
<a
|
||||
:href="contactProfileLink"
|
||||
class="text-base"
|
||||
target="_blank"
|
||||
rel="noopener nofollow noreferrer"
|
||||
>
|
||||
<woot-button
|
||||
size="tiny"
|
||||
icon="open"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="additionalAttributes.description" class="break-words mb-0.5">
|
||||
{{ additionalAttributes.description }}
|
||||
</p>
|
||||
<div class="flex flex-col items-start w-full gap-2">
|
||||
<ContactInfoRow
|
||||
:href="contact.email ? `mailto:${contact.email}` : ''"
|
||||
:value="contact.email"
|
||||
icon="mail"
|
||||
emoji="✉️"
|
||||
:title="$t('CONTACT_PANEL.EMAIL_ADDRESS')"
|
||||
show-copy
|
||||
/>
|
||||
<ContactInfoRow
|
||||
:href="contact.phone_number ? `tel:${contact.phone_number}` : ''"
|
||||
:value="contact.phone_number"
|
||||
icon="call"
|
||||
emoji="📞"
|
||||
:title="$t('CONTACT_PANEL.PHONE_NUMBER')"
|
||||
show-copy
|
||||
/>
|
||||
<ContactInfoRow
|
||||
v-if="contact.identifier"
|
||||
:value="contact.identifier"
|
||||
icon="contact-identify"
|
||||
emoji="🪪"
|
||||
:title="$t('CONTACT_PANEL.IDENTIFIER')"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
:value="additionalAttributes.company_name"
|
||||
icon="building-bank"
|
||||
emoji="🏢"
|
||||
:title="$t('CONTACT_PANEL.COMPANY')"
|
||||
/>
|
||||
<ContactInfoRow
|
||||
v-if="location || additionalAttributes.location"
|
||||
:value="location || additionalAttributes.location"
|
||||
icon="map"
|
||||
emoji="🌍"
|
||||
:title="$t('CONTACT_PANEL.LOCATION')"
|
||||
/>
|
||||
<SocialIcons :social-profiles="socialProfiles" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full mt-0.5 gap-2">
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
:title="$t('CONTACT_PANEL.NEW_MESSAGE')"
|
||||
icon="chat"
|
||||
size="small"
|
||||
@click="toggleConversationModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
:title="$t('EDIT_CONTACT.BUTTON_LABEL')"
|
||||
icon="edit"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="toggleEditModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.MERGE_CONTACT')"
|
||||
:title="$t('CONTACT_PANEL.MERGE_CONTACT')"
|
||||
icon="merge"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
:disabled="uiFlags.isMerging"
|
||||
@click="openMergeModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
v-tooltip="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
:title="$t('DELETE_CONTACT.BUTTON_LABEL')"
|
||||
icon="delete"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="alert"
|
||||
:disabled="uiFlags.isDeleting"
|
||||
@click="toggleDeleteModal"
|
||||
/>
|
||||
</div>
|
||||
<EditContact
|
||||
v-if="showEditModal"
|
||||
:show="showEditModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleEditModal"
|
||||
/>
|
||||
<NewConversation
|
||||
v-if="contact.id"
|
||||
:show="showConversationModal"
|
||||
:contact="contact"
|
||||
@cancel="toggleConversationModal"
|
||||
/>
|
||||
<ContactMergeModal
|
||||
v-if="showMergeModal"
|
||||
:primary-contact="contact"
|
||||
:show="showMergeModal"
|
||||
@close="toggleMergeModal"
|
||||
/>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
v-if="showDeleteModal"
|
||||
:show.sync="showDeleteModal"
|
||||
:on-close="closeDelete"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('DELETE_CONTACT.CONFIRM.TITLE')"
|
||||
:message="$t('DELETE_CONTACT.CONFIRM.MESSAGE')"
|
||||
:message-value="confirmDeleteMessage"
|
||||
:confirm-text="$t('DELETE_CONTACT.CONFIRM.YES')"
|
||||
:reject-text="$t('DELETE_CONTACT.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,63 +1,5 @@
|
||||
<template>
|
||||
<div class="ltr:-ml-1 rtl:-mr-1 h-5 w-full">
|
||||
<a
|
||||
v-if="href"
|
||||
:href="href"
|
||||
class="flex items-center gap-2 text-slate-800 dark:text-slate-100 hover:underline"
|
||||
>
|
||||
<emoji-or-icon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="ltr:ml-1 rtl:mr-1 flex-shrink-0"
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
class="overflow-hidden whitespace-nowrap text-ellipsis text-sm"
|
||||
:title="value"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
<span v-else class="text-slate-300 dark:text-slate-600 text-sm">{{
|
||||
$t('CONTACT_PANEL.NOT_AVAILABLE')
|
||||
}}</span>
|
||||
|
||||
<woot-button
|
||||
v-if="showCopy"
|
||||
type="submit"
|
||||
variant="clear"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="clipboard"
|
||||
class-names="p-0"
|
||||
@click="onCopy"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-2 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
<emoji-or-icon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="ltr:ml-1 rtl:mr-1 flex-shrink-0"
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
class="overflow-hidden whitespace-nowrap text-ellipsis text-sm"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
<span v-else class="text-slate-300 dark:text-slate-600 text-sm">{{
|
||||
$t('CONTACT_PANEL.NOT_AVAILABLE')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
|
||||
@@ -65,7 +7,6 @@ export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
@@ -92,8 +33,67 @@ export default {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.value);
|
||||
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-5 ltr:-ml-1 rtl:-mr-1">
|
||||
<a
|
||||
v-if="href"
|
||||
:href="href"
|
||||
class="flex items-center gap-2 text-slate-800 dark:text-slate-100 hover:underline"
|
||||
>
|
||||
<EmojiOrIcon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
|
||||
:title="value"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
<span v-else class="text-sm text-slate-300 dark:text-slate-600">{{
|
||||
$t('CONTACT_PANEL.NOT_AVAILABLE')
|
||||
}}</span>
|
||||
|
||||
<woot-button
|
||||
v-if="showCopy"
|
||||
type="submit"
|
||||
variant="clear"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="clipboard"
|
||||
class-names="p-0"
|
||||
@click="onCopy"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-2 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
<EmojiOrIcon
|
||||
:icon="icon"
|
||||
:emoji="emoji"
|
||||
icon-size="14"
|
||||
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
|
||||
/>
|
||||
<span
|
||||
v-if="value"
|
||||
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
<span v-else class="text-sm text-slate-300 dark:text-slate-600">{{
|
||||
$t('CONTACT_PANEL.NOT_AVAILABLE')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+275
-270
@@ -1,244 +1,7 @@
|
||||
<template>
|
||||
<form class="conversation--form w-full" @submit.prevent="onFormSubmit">
|
||||
<div
|
||||
v-if="showNoInboxAlert"
|
||||
class="relative mx-0 mt-0 mb-2.5 p-2 rounded-none text-sm border border-solid border-yellow-500 dark:border-yellow-700 bg-yellow-200/60 dark:bg-yellow-200/20 text-slate-700 dark:text-yellow-400"
|
||||
>
|
||||
<p class="mb-0">
|
||||
{{ $t('NEW_CONVERSATION.NO_INBOX') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="gap-2 flex flex-row">
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }}
|
||||
</label>
|
||||
<div
|
||||
class="multiselect-wrap--small"
|
||||
:class="{ 'has-multi-select-error': $v.targetInbox.$error }"
|
||||
>
|
||||
<multiselect
|
||||
v-model="targetInbox"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||
selected-label=""
|
||||
select-label=""
|
||||
deselect-label=""
|
||||
:max-height="160"
|
||||
:close-on-select="true"
|
||||
:options="[...inboxes]"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="{ option }">
|
||||
<inbox-dropdown-item
|
||||
v-if="option.name"
|
||||
:name="option.name"
|
||||
:inbox-identifier="computedInboxSource(option)"
|
||||
:channel-type="option.channel_type"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.PLACEHOLDER') }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="option" slot-scope="{ option }">
|
||||
<inbox-dropdown-item
|
||||
:name="option.name"
|
||||
:inbox-identifier="computedInboxSource(option)"
|
||||
:channel-type="option.channel_type"
|
||||
/>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
<label :class="{ error: $v.targetInbox.$error }">
|
||||
<span v-if="$v.targetInbox.$error" class="message">
|
||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
{{ $t('NEW_CONVERSATION.FORM.TO.LABEL') }}
|
||||
<div
|
||||
class="flex items-center h-[2.4735rem] rounded-sm py-1 px-2 bg-slate-25 dark:bg-slate-900 border border-solid border-slate-75 dark:border-slate-600"
|
||||
>
|
||||
<thumbnail
|
||||
:src="contact.thumbnail"
|
||||
size="24px"
|
||||
:username="contact.name"
|
||||
:status="contact.availability_status"
|
||||
/>
|
||||
<h4
|
||||
class="m-0 ml-2 mr-2 text-slate-700 dark:text-slate-100 text-sm"
|
||||
>
|
||||
{{ contact.name }}
|
||||
</h4>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isAnEmailInbox" class="w-full">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: $v.subject.$error }">
|
||||
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.LABEL') }}
|
||||
<input
|
||||
v-model="subject"
|
||||
type="text"
|
||||
:placeholder="$t('NEW_CONVERSATION.FORM.SUBJECT.PLACEHOLDER')"
|
||||
@input="$v.subject.$touch"
|
||||
/>
|
||||
<span v-if="$v.subject.$error" class="message">
|
||||
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="w-full">
|
||||
<div class="relative">
|
||||
<canned-response
|
||||
v-if="showCannedResponseMenu && hasSlashCommand"
|
||||
:search-key="cannedResponseSearchKey"
|
||||
@click="replaceTextWithCannedResponse"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isEmailOrWebWidgetInbox">
|
||||
<label>
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
||||
</label>
|
||||
<reply-email-head
|
||||
v-if="isAnEmailInbox"
|
||||
:cc-emails.sync="ccEmails"
|
||||
:bcc-emails.sync="bccEmails"
|
||||
/>
|
||||
<div class="editor-wrap">
|
||||
<woot-message-editor
|
||||
v-model="message"
|
||||
class="message-editor"
|
||||
:class="{ editor_warning: $v.message.$error }"
|
||||
:enable-variables="true"
|
||||
:signature="signatureToApply"
|
||||
:allow-signature="true"
|
||||
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
||||
@toggle-canned-menu="toggleCannedMenu"
|
||||
@blur="$v.message.$touch"
|
||||
>
|
||||
<template #footer>
|
||||
<message-signature-missing-alert
|
||||
v-if="isSignatureEnabledForInbox && !messageSignature"
|
||||
class="!mx-0 mb-1"
|
||||
/>
|
||||
<div v-if="isAnEmailInbox" class="mb-3 mt-px">
|
||||
<woot-button
|
||||
v-tooltip.top-end="signatureToggleTooltip"
|
||||
icon="signature"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:title="signatureToggleTooltip"
|
||||
@click.prevent="toggleMessageSignature"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</woot-message-editor>
|
||||
<span v-if="$v.message.$error" class="editor-warning__message">
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<whatsapp-templates
|
||||
v-else-if="hasWhatsappTemplates"
|
||||
:inbox-id="selectedInbox.inbox.id"
|
||||
@on-select-template="toggleWaTemplate"
|
||||
@on-send="onSendWhatsAppReply"
|
||||
/>
|
||||
<label v-else :class="{ error: $v.message.$error }">
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
||||
<textarea
|
||||
v-model="message"
|
||||
class="min-h-[5rem]"
|
||||
type="text"
|
||||
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
||||
@input="$v.message.$touch"
|
||||
/>
|
||||
<span v-if="$v.message.$error" class="message">
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="isEmailOrWebWidgetInbox" class="flex flex-col">
|
||||
<file-upload
|
||||
ref="uploadAttachment"
|
||||
input-id="newConversationAttachment"
|
||||
:size="4096 * 4096"
|
||||
:accept="allowedFileTypes"
|
||||
:multiple="true"
|
||||
:drop="true"
|
||||
:drop-directory="false"
|
||||
:data="{
|
||||
direct_upload_url: '/rails/active_storage/direct_uploads',
|
||||
direct_upload: true,
|
||||
}"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<woot-button
|
||||
class-names="button--upload"
|
||||
icon="attach"
|
||||
emoji="📎"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
>
|
||||
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.SELECT') }}
|
||||
</woot-button>
|
||||
<span
|
||||
class="text-slate-500 ltr:ml-1 rtl:mr-1 font-medium text-xs dark:text-slate-400"
|
||||
>
|
||||
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.HELP_TEXT') }}
|
||||
</span>
|
||||
</file-upload>
|
||||
<div
|
||||
v-if="hasAttachments"
|
||||
class="max-h-20 overflow-y-auto mb-4 mt-1.5"
|
||||
>
|
||||
<attachment-preview
|
||||
class="[&>.preview-item]:dark:bg-slate-700 flex-row flex-wrap gap-x-3 gap-y-1"
|
||||
:attachments="attachedFiles"
|
||||
:remove-attachment="removeAttachment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!hasWhatsappTemplates"
|
||||
class="flex flex-row justify-end gap-2 py-2 px-0 w-full"
|
||||
>
|
||||
<button class="button clear" @click.prevent="onCancel">
|
||||
{{ $t('NEW_CONVERSATION.FORM.CANCEL') }}
|
||||
</button>
|
||||
<woot-button type="submit" :is-loading="conversationsUiFlags.isCreating">
|
||||
{{ $t('NEW_CONVERSATION.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
<transition v-if="isEmailOrWebWidgetInbox" name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.uploadAttachment && $refs.uploadAttachment.dropActive"
|
||||
class="flex top-0 bottom-0 z-30 gap-2 right-0 left-0 items-center justify-center flex-col absolute w-full h-full bg-white/80 dark:bg-slate-700/80"
|
||||
>
|
||||
<fluent-icon icon="cloud-backup" size="40" />
|
||||
<h4 class="text-2xl break-words text-slate-600 dark:text-slate-200">
|
||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||
</h4>
|
||||
</div>
|
||||
</transition>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead.vue';
|
||||
@@ -246,11 +9,11 @@ import CannedResponse from 'dashboard/components/widgets/conversation/CannedResp
|
||||
import MessageSignatureMissingAlert from 'dashboard/components/widgets/conversation/MessageSignatureMissingAlert';
|
||||
import InboxDropdownItem from 'dashboard/components/widgets/InboxDropdownItem.vue';
|
||||
import WhatsappTemplates from './WhatsappTemplates.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
|
||||
import { getInboxSource } from 'dashboard/helper/inbox';
|
||||
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, requiredIf } from '@vuelidate/validators';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import FileUpload from 'vue-upload-component';
|
||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||
@@ -260,7 +23,6 @@ import {
|
||||
appendSignature,
|
||||
removeSignature,
|
||||
} from 'dashboard/helper/editorHelper';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -274,7 +36,7 @@ export default {
|
||||
AttachmentPreview,
|
||||
MessageSignatureMissingAlert,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin, inboxMixin, fileUploadMixin],
|
||||
mixins: [inboxMixin, fileUploadMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
@@ -284,10 +46,13 @@ export default {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
channelType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox } =
|
||||
useUISettings();
|
||||
const v$ = useVuelidate();
|
||||
|
||||
return { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox, v$ };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -303,16 +68,18 @@ export default {
|
||||
attachedFiles: [],
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
subject: {
|
||||
required: requiredIf('isAnEmailInbox'),
|
||||
},
|
||||
message: {
|
||||
required,
|
||||
},
|
||||
targetInbox: {
|
||||
required,
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
subject: {
|
||||
required: requiredIf(this.isAnEmailInbox),
|
||||
},
|
||||
message: {
|
||||
required,
|
||||
},
|
||||
targetInbox: {
|
||||
required,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -323,7 +90,7 @@ export default {
|
||||
messageSignature: 'getMessageSignature',
|
||||
}),
|
||||
sendWithSignature() {
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
||||
},
|
||||
signatureToApply() {
|
||||
return this.messageSignature;
|
||||
@@ -468,10 +235,8 @@ export default {
|
||||
});
|
||||
};
|
||||
},
|
||||
removeAttachment(itemIndex) {
|
||||
this.attachedFiles = this.attachedFiles.filter(
|
||||
(item, index) => itemIndex !== index
|
||||
);
|
||||
removeAttachment(attachments) {
|
||||
this.attachedFiles = attachments;
|
||||
},
|
||||
onCancel() {
|
||||
this.$emit('cancel');
|
||||
@@ -497,8 +262,8 @@ export default {
|
||||
},
|
||||
onFormSubmit() {
|
||||
const isFromWhatsApp = false;
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
this.createConversation({
|
||||
@@ -515,15 +280,12 @@ export default {
|
||||
message: this.$t('NEW_CONVERSATION.FORM.GO_TO_CONVERSATION'),
|
||||
};
|
||||
this.onSuccess();
|
||||
this.showAlert(
|
||||
this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'),
|
||||
action
|
||||
);
|
||||
useAlert(this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'), action);
|
||||
} catch (error) {
|
||||
if (error instanceof ExceptionWithMessage) {
|
||||
this.showAlert(error.data);
|
||||
useAlert(error.data);
|
||||
} else {
|
||||
this.showAlert(this.$t('NEW_CONVERSATION.FORM.ERROR_MESSAGE'));
|
||||
useAlert(this.$t('NEW_CONVERSATION.FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -556,6 +318,246 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/prefer-true-attribute-shorthand -->
|
||||
<template>
|
||||
<form class="w-full conversation--form" @submit.prevent="onFormSubmit">
|
||||
<div
|
||||
v-if="showNoInboxAlert"
|
||||
class="relative mx-0 mt-0 mb-2.5 p-2 rounded-none text-sm border border-solid border-yellow-500 dark:border-yellow-700 bg-yellow-200/60 dark:bg-yellow-200/20 text-slate-700 dark:text-yellow-400"
|
||||
>
|
||||
<p class="mb-0">
|
||||
{{ $t('NEW_CONVERSATION.NO_INBOX') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }}
|
||||
</label>
|
||||
<div
|
||||
class="multiselect-wrap--small"
|
||||
:class="{ 'has-multi-select-error': v$.targetInbox.$error }"
|
||||
>
|
||||
<multiselect
|
||||
v-model="targetInbox"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||
selected-label=""
|
||||
select-label=""
|
||||
deselect-label=""
|
||||
:max-height="160"
|
||||
close-on-select
|
||||
:options="[...inboxes]"
|
||||
>
|
||||
<template slot="singleLabel" slot-scope="{ option }">
|
||||
<InboxDropdownItem
|
||||
v-if="option.name"
|
||||
:name="option.name"
|
||||
:inbox-identifier="computedInboxSource(option)"
|
||||
:channel-type="option.channel_type"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.PLACEHOLDER') }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="option" slot-scope="{ option }">
|
||||
<InboxDropdownItem
|
||||
:name="option.name"
|
||||
:inbox-identifier="computedInboxSource(option)"
|
||||
:channel-type="option.channel_type"
|
||||
/>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
<label :class="{ error: v$.targetInbox.$error }">
|
||||
<span v-if="v$.targetInbox.$error" class="message">
|
||||
{{ $t('NEW_CONVERSATION.FORM.INBOX.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
{{ $t('NEW_CONVERSATION.FORM.TO.LABEL') }}
|
||||
<div
|
||||
class="flex items-center h-[2.4735rem] rounded-sm py-1 px-2 bg-slate-25 dark:bg-slate-900 border border-solid border-slate-75 dark:border-slate-600"
|
||||
>
|
||||
<Thumbnail
|
||||
:src="contact.thumbnail"
|
||||
size="24px"
|
||||
:username="contact.name"
|
||||
:status="contact.availability_status"
|
||||
/>
|
||||
<h4
|
||||
class="m-0 ml-2 mr-2 text-sm text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
{{ contact.name }}
|
||||
</h4>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isAnEmailInbox" class="w-full">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.subject.$error }">
|
||||
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.LABEL') }}
|
||||
<input
|
||||
v-model="subject"
|
||||
type="text"
|
||||
:placeholder="$t('NEW_CONVERSATION.FORM.SUBJECT.PLACEHOLDER')"
|
||||
@input="v$.subject.$touch"
|
||||
/>
|
||||
<span v-if="v$.subject.$error" class="message">
|
||||
{{ $t('NEW_CONVERSATION.FORM.SUBJECT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="w-full">
|
||||
<div class="relative">
|
||||
<CannedResponse
|
||||
v-if="showCannedResponseMenu && hasSlashCommand"
|
||||
:search-key="cannedResponseSearchKey"
|
||||
@click="replaceTextWithCannedResponse"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isEmailOrWebWidgetInbox">
|
||||
<label>
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
||||
</label>
|
||||
<ReplyEmailHead
|
||||
v-if="isAnEmailInbox"
|
||||
:cc-emails.sync="ccEmails"
|
||||
:bcc-emails.sync="bccEmails"
|
||||
/>
|
||||
<div class="editor-wrap">
|
||||
<WootMessageEditor
|
||||
v-model="message"
|
||||
class="message-editor"
|
||||
:class="{ editor_warning: v$.message.$error }"
|
||||
enable-variables
|
||||
:signature="signatureToApply"
|
||||
allow-signature
|
||||
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
||||
@toggle-canned-menu="toggleCannedMenu"
|
||||
@blur="v$.message.$touch"
|
||||
>
|
||||
<template #footer>
|
||||
<MessageSignatureMissingAlert
|
||||
v-if="isSignatureEnabledForInbox && !messageSignature"
|
||||
class="!mx-0 mb-1"
|
||||
/>
|
||||
<div v-if="isAnEmailInbox" class="mt-px mb-3">
|
||||
<woot-button
|
||||
v-tooltip.top-end="signatureToggleTooltip"
|
||||
icon="signature"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
:title="signatureToggleTooltip"
|
||||
@click.prevent="toggleMessageSignature"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</WootMessageEditor>
|
||||
<span v-if="v$.message.$error" class="editor-warning__message">
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<WhatsappTemplates
|
||||
v-else-if="hasWhatsappTemplates"
|
||||
:inbox-id="selectedInbox.inbox.id"
|
||||
@on-select-template="toggleWaTemplate"
|
||||
@onSend="onSendWhatsAppReply"
|
||||
/>
|
||||
<label v-else :class="{ error: v$.message.$error }">
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
|
||||
<textarea
|
||||
v-model="message"
|
||||
class="min-h-[5rem]"
|
||||
type="text"
|
||||
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
|
||||
@input="v$.message.$touch"
|
||||
/>
|
||||
<span v-if="v$.message.$error" class="message">
|
||||
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<div v-if="isEmailOrWebWidgetInbox" class="flex flex-col">
|
||||
<FileUpload
|
||||
ref="uploadAttachment"
|
||||
input-id="newConversationAttachment"
|
||||
:size="4096 * 4096"
|
||||
:accept="allowedFileTypes"
|
||||
multiple
|
||||
:drop="true"
|
||||
:drop-directory="false"
|
||||
:data="{
|
||||
direct_upload_url: '/rails/active_storage/direct_uploads',
|
||||
direct_upload: true,
|
||||
}"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<woot-button
|
||||
class-names="button--upload"
|
||||
icon="attach"
|
||||
emoji="📎"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
>
|
||||
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.SELECT') }}
|
||||
</woot-button>
|
||||
<span
|
||||
class="text-xs font-medium text-slate-500 ltr:ml-1 rtl:mr-1 dark:text-slate-400"
|
||||
>
|
||||
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.HELP_TEXT') }}
|
||||
</span>
|
||||
</FileUpload>
|
||||
<div
|
||||
v-if="hasAttachments"
|
||||
class="max-h-20 overflow-y-auto mb-4 mt-1.5"
|
||||
>
|
||||
<AttachmentPreview
|
||||
class="[&>.preview-item]:dark:bg-slate-700 flex-row flex-wrap gap-x-3 gap-y-1"
|
||||
:attachments="attachedFiles"
|
||||
@removeAttachment="removeAttachment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!hasWhatsappTemplates"
|
||||
class="flex flex-row justify-end w-full gap-2 px-0 py-2"
|
||||
>
|
||||
<button class="button clear" @click.prevent="onCancel">
|
||||
{{ $t('NEW_CONVERSATION.FORM.CANCEL') }}
|
||||
</button>
|
||||
<woot-button type="submit" :is-loading="conversationsUiFlags.isCreating">
|
||||
{{ $t('NEW_CONVERSATION.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
<transition v-if="isEmailOrWebWidgetInbox" name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.uploadAttachment && $refs.uploadAttachment.dropActive"
|
||||
class="absolute top-0 bottom-0 left-0 right-0 z-30 flex flex-col items-center justify-center w-full h-full gap-2 bg-white/80 dark:bg-slate-700/80"
|
||||
>
|
||||
<fluent-icon icon="cloud-backup" size="40" />
|
||||
<h4 class="text-2xl break-words text-slate-600 dark:text-slate-200">
|
||||
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
|
||||
</h4>
|
||||
</div>
|
||||
</transition>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversation--form {
|
||||
@apply pt-4 px-8 pb-8;
|
||||
@@ -574,6 +576,7 @@ export default {
|
||||
.file-uploads {
|
||||
@apply text-start;
|
||||
}
|
||||
|
||||
.multiselect-wrap--small.has-multi-select-error {
|
||||
::v-deep {
|
||||
.multiselect__tags {
|
||||
@@ -586,9 +589,11 @@ export default {
|
||||
.mention--box {
|
||||
@apply left-0 m-auto right-0 top-auto h-fit;
|
||||
}
|
||||
|
||||
.multiselect .multiselect__content .multiselect__option span {
|
||||
@apply inline-flex w-6 text-slate-600 dark:text-slate-400;
|
||||
}
|
||||
|
||||
.multiselect .multiselect__content .multiselect__option {
|
||||
@apply py-0.5 px-1;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onCancel" modal-type="right-aligned">
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CREATE_CONTACT.TITLE')"
|
||||
:header-content="$t('CREATE_CONTACT.DESC')"
|
||||
/>
|
||||
<contact-form
|
||||
:in-progress="uiFlags.isCreating"
|
||||
:on-submit="onSubmit"
|
||||
@success="onSuccess"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactForm from './ContactForm.vue';
|
||||
@@ -29,10 +11,6 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -54,3 +32,21 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onCancel" modal-type="right-aligned">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CREATE_CONTACT.TITLE')"
|
||||
:header-content="$t('CREATE_CONTACT.DESC')"
|
||||
/>
|
||||
<ContactForm
|
||||
:in-progress="uiFlags.isCreating"
|
||||
:on-submit="onSubmit"
|
||||
@success="onSuccess"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onCancel" modal-type="right-aligned">
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<woot-modal-header
|
||||
:header-title="`${$t('EDIT_CONTACT.TITLE')} - ${
|
||||
contact.name || contact.email
|
||||
}`"
|
||||
:header-content="$t('EDIT_CONTACT.DESC')"
|
||||
/>
|
||||
<contact-form
|
||||
:contact="contact"
|
||||
:in-progress="uiFlags.isUpdating"
|
||||
:on-submit="onSubmit"
|
||||
@success="onSuccess"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactForm from './ContactForm.vue';
|
||||
@@ -61,3 +40,24 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onCancel" modal-type="right-aligned">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="`${$t('EDIT_CONTACT.TITLE')} - ${
|
||||
contact.name || contact.email
|
||||
}`"
|
||||
:header-content="$t('EDIT_CONTACT.DESC')"
|
||||
/>
|
||||
<ContactForm
|
||||
:contact="contact"
|
||||
:in-progress="uiFlags.isUpdating"
|
||||
:on-submit="onSubmit"
|
||||
@success="onSuccess"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
+18
-18
@@ -1,21 +1,3 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onCancel">
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<woot-modal-header
|
||||
:header-title="$t('NEW_CONVERSATION.TITLE')"
|
||||
:header-content="$t('NEW_CONVERSATION.DESC')"
|
||||
/>
|
||||
<conversation-form
|
||||
:contact="contact"
|
||||
:on-submit="onSubmit"
|
||||
@success="onSuccess"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConversationForm from './ConversationForm.vue';
|
||||
|
||||
@@ -59,3 +41,21 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onCancel">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="$t('NEW_CONVERSATION.TITLE')"
|
||||
:header-content="$t('NEW_CONVERSATION.DESC')"
|
||||
/>
|
||||
<ConversationForm
|
||||
:contact="contact"
|
||||
:on-submit="onSubmit"
|
||||
@success="onSuccess"
|
||||
@cancel="onCancel"
|
||||
/>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
<template>
|
||||
<div v-if="availableProfiles.length" class="flex items-end mx-0 my-2 gap-3">
|
||||
<a
|
||||
v-for="profile in availableProfiles"
|
||||
:key="profile.key"
|
||||
:href="`${profile.link}${socialProfiles[profile.key]}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="`brand-${profile.key}`"
|
||||
size="16"
|
||||
class="text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
@@ -44,3 +26,21 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="availableProfiles.length" class="flex items-end mx-0 my-2 gap-3">
|
||||
<a
|
||||
v-for="profile in availableProfiles"
|
||||
:key="profile.key"
|
||||
:href="`${profile.link}${socialProfiles[profile.key]}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="`brand-${profile.key}`"
|
||||
size="16"
|
||||
class="text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+16
-22
@@ -1,19 +1,3 @@
|
||||
<template>
|
||||
<div class="mx-0 flex flex-wrap">
|
||||
<templates-picker
|
||||
v-if="!selectedWaTemplate"
|
||||
:inbox-id="inboxId"
|
||||
@onSelect="pickTemplate"
|
||||
/>
|
||||
<template-parser
|
||||
v-else
|
||||
:template="selectedWaTemplate"
|
||||
@resetTemplate="onResetTemplate"
|
||||
@sendMessage="onSendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TemplatesPicker from 'dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue';
|
||||
import TemplateParser from 'dashboard/components/widgets/conversation/WhatsappTemplates/TemplateParser.vue';
|
||||
@@ -27,10 +11,6 @@ export default {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -47,7 +27,7 @@ export default {
|
||||
this.selectedWaTemplate = null;
|
||||
},
|
||||
onSendMessage(message) {
|
||||
this.$emit('on-send', message);
|
||||
this.$emit('onSend', message);
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('cancel');
|
||||
@@ -56,4 +36,18 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<div class="flex flex-wrap mx-0">
|
||||
<TemplatesPicker
|
||||
v-if="!selectedWaTemplate"
|
||||
:inbox-id="inboxId"
|
||||
@onSelect="pickTemplate"
|
||||
/>
|
||||
<TemplateParser
|
||||
v-else
|
||||
:template="selectedWaTemplate"
|
||||
@resetTemplate="onResetTemplate"
|
||||
@sendMessage="onSendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/* eslint arrow-body-style: 0 */
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
const ConversationView = () => import('./ConversationView');
|
||||
const ConversationView = () => import('./ConversationView.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/dashboard'),
|
||||
name: 'home',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => {
|
||||
return { inboxId: 0 };
|
||||
@@ -16,7 +18,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
|
||||
name: 'inbox_conversation',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return { inboxId: 0, conversationId: route.params.conversation_id };
|
||||
@@ -25,7 +29,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
|
||||
name: 'inbox_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return { inboxId: route.params.inbox_id };
|
||||
@@ -36,7 +42,9 @@ export default {
|
||||
'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversation_through_inbox',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return {
|
||||
@@ -48,7 +56,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/label/:label'),
|
||||
name: 'label_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ label: route.params.label }),
|
||||
},
|
||||
@@ -57,7 +67,9 @@ export default {
|
||||
'accounts/:accountId/label/:label/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversations_through_label',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversation_id,
|
||||
@@ -67,7 +79,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/team/:teamId'),
|
||||
name: 'team_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ teamId: route.params.teamId }),
|
||||
},
|
||||
@@ -76,7 +90,9 @@ export default {
|
||||
'accounts/:accountId/team/:teamId/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversations_through_team',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -86,7 +102,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/custom_view/:id'),
|
||||
name: 'folder_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ foldersId: route.params.id }),
|
||||
},
|
||||
@@ -95,7 +113,9 @@ export default {
|
||||
'accounts/:accountId/custom_view/:id/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversations_through_folders',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversation_id,
|
||||
@@ -105,7 +125,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/mentions/conversations'),
|
||||
name: 'conversation_mentions',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'mention' }),
|
||||
},
|
||||
@@ -114,7 +136,9 @@ export default {
|
||||
'accounts/:accountId/mentions/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_mentions',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -124,7 +148,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/unattended/conversations'),
|
||||
name: 'conversation_unattended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'unattended' }),
|
||||
},
|
||||
@@ -133,7 +159,9 @@ export default {
|
||||
'accounts/:accountId/unattended/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_unattended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -143,7 +171,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/participating/conversations'),
|
||||
name: 'conversation_participating',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'participating' }),
|
||||
},
|
||||
@@ -152,7 +182,9 @@ export default {
|
||||
'accounts/:accountId/participating/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_participating',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
|
||||
+60
-52
@@ -1,57 +1,15 @@
|
||||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<custom-attribute
|
||||
v-for="attribute in displayedAttributes"
|
||||
:key="attribute.id"
|
||||
:attribute-key="attribute.attribute_key"
|
||||
:attribute-type="attribute.attribute_display_type"
|
||||
:values="attribute.attribute_values"
|
||||
:label="attribute.attribute_display_name"
|
||||
:description="attribute.attribute_description"
|
||||
:value="attribute.value"
|
||||
:show-actions="true"
|
||||
:attribute-regex="attribute.regex_pattern"
|
||||
:regex-cue="attribute.regex_cue"
|
||||
:class="attributeClass"
|
||||
:contact-id="contactId"
|
||||
@update="onUpdate"
|
||||
@delete="onDelete"
|
||||
@copy="onCopy"
|
||||
/>
|
||||
<p
|
||||
v-if="!displayedAttributes.length && emptyStateMessage"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
{{ emptyStateMessage }}
|
||||
</p>
|
||||
<!-- Show more and show less buttons show it if the filteredAttributes length is greater than 5 -->
|
||||
<div v-if="filteredAttributes.length > 5" class="flex px-2 py-2">
|
||||
<woot-button
|
||||
size="small"
|
||||
:icon="showAllAttributes ? 'chevron-up' : 'chevron-down'"
|
||||
variant="clear"
|
||||
color-scheme="primary"
|
||||
class="!px-2 hover:!bg-transparent dark:hover:!bg-transparent"
|
||||
@click="onClickToggle"
|
||||
>
|
||||
{{ toggleButtonText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
|
||||
import attributeMixin from 'dashboard/mixins/attributeMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CustomAttribute,
|
||||
},
|
||||
mixins: [alertMixin, attributeMixin, uiSettingsMixin],
|
||||
mixins: [attributeMixin],
|
||||
props: {
|
||||
attributeType: {
|
||||
type: String,
|
||||
@@ -71,6 +29,14 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAllAttributes: false,
|
||||
@@ -141,12 +107,12 @@ export default {
|
||||
custom_attributes: updatedAttributes,
|
||||
});
|
||||
}
|
||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
|
||||
useAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.ERROR');
|
||||
this.showAlert(errorMessage);
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
async onDelete(key) {
|
||||
@@ -164,22 +130,64 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
|
||||
useAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.ERROR');
|
||||
this.showAlert(errorMessage);
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
async onCopy(attributeValue) {
|
||||
await copyTextToClipboard(attributeValue);
|
||||
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
|
||||
useAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="custom-attributes--panel">
|
||||
<CustomAttribute
|
||||
v-for="attribute in displayedAttributes"
|
||||
:key="attribute.id"
|
||||
:attribute-key="attribute.attribute_key"
|
||||
:attribute-type="attribute.attribute_display_type"
|
||||
:values="attribute.attribute_values"
|
||||
:label="attribute.attribute_display_name"
|
||||
:description="attribute.attribute_description"
|
||||
:value="attribute.value"
|
||||
show-actions
|
||||
:attribute-regex="attribute.regex_pattern"
|
||||
:regex-cue="attribute.regex_cue"
|
||||
:class="attributeClass"
|
||||
:contact-id="contactId"
|
||||
@update="onUpdate"
|
||||
@delete="onDelete"
|
||||
@copy="onCopy"
|
||||
/>
|
||||
<p
|
||||
v-if="!displayedAttributes.length && emptyStateMessage"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
{{ emptyStateMessage }}
|
||||
</p>
|
||||
<!-- Show more and show less buttons show it if the filteredAttributes length is greater than 5 -->
|
||||
<div v-if="filteredAttributes.length > 5" class="flex px-2 py-2">
|
||||
<woot-button
|
||||
size="small"
|
||||
:icon="showAllAttributes ? 'chevron-up' : 'chevron-down'"
|
||||
variant="clear"
|
||||
color-scheme="primary"
|
||||
class="!px-2 hover:!bg-transparent dark:hover:!bg-transparent"
|
||||
@click="onClickToggle"
|
||||
>
|
||||
{{ toggleButtonText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-attributes--panel {
|
||||
.conversation--attribute {
|
||||
|
||||
@@ -1,5 +1,85 @@
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
LabelDropdown,
|
||||
AddLabel,
|
||||
},
|
||||
|
||||
mixins: [conversationLabelMixin],
|
||||
props: {
|
||||
// conversationId prop is used in /conversation/labelMixin,
|
||||
// remove this props when refactoring to composable if not needed
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
conversationId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
const conversationLabelBoxRef = ref(null);
|
||||
const showSearchDropdownLabel = ref(false);
|
||||
|
||||
const toggleLabels = () => {
|
||||
showSearchDropdownLabel.value = !showSearchDropdownLabel.value;
|
||||
};
|
||||
|
||||
const closeDropdownLabel = () => {
|
||||
showSearchDropdownLabel.value = false;
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
toggleLabels();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => {
|
||||
if (showSearchDropdownLabel.value) {
|
||||
toggleLabels();
|
||||
}
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, conversationLabelBoxRef);
|
||||
return {
|
||||
isAdmin,
|
||||
conversationLabelBoxRef,
|
||||
showSearchDropdownLabel,
|
||||
closeDropdownLabel,
|
||||
toggleLabels,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLabels: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
conversationUiFlags: 'conversationLabels/getUIFlags',
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-labels-wrap">
|
||||
<div ref="conversationLabelBoxRef" class="sidebar-labels-wrap">
|
||||
<div
|
||||
v-if="!conversationUiFlags.isFetching"
|
||||
class="contact-conversation--list"
|
||||
@@ -9,13 +89,13 @@
|
||||
class="label-wrap"
|
||||
@keyup.esc="closeDropdownLabel"
|
||||
>
|
||||
<add-label @add="toggleLabels" />
|
||||
<AddLabel @add="toggleLabels" />
|
||||
<woot-label
|
||||
v-for="label in activeLabels"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:show-close="true"
|
||||
show-close
|
||||
:color="label.color"
|
||||
variant="smooth"
|
||||
class="max-w-[calc(100%-0.5rem)]"
|
||||
@@ -27,7 +107,7 @@
|
||||
:class="{ 'dropdown-pane--open': showSearchDropdownLabel }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<label-dropdown
|
||||
<LabelDropdown
|
||||
v-if="showSearchDropdownLabel"
|
||||
:account-labels="accountLabels"
|
||||
:selected-labels="savedLabels"
|
||||
@@ -39,76 +119,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<spinner v-else />
|
||||
<Spinner v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
LabelDropdown,
|
||||
AddLabel,
|
||||
},
|
||||
|
||||
mixins: [conversationLabelMixin, adminMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedLabels: [],
|
||||
showSearchDropdownLabel: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
conversationUiFlags: 'conversationLabels/getUIFlags',
|
||||
labelUiFlags: 'conversationLabels/getUIFlags',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
toggleLabels() {
|
||||
this.showSearchDropdownLabel = !this.showSearchDropdownLabel;
|
||||
},
|
||||
closeDropdownLabel() {
|
||||
this.showSearchDropdownLabel = false;
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.toggleLabels();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => {
|
||||
if (this.showSearchDropdownLabel) {
|
||||
this.toggleLabels();
|
||||
}
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-labels-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -1,40 +1,5 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex px-4 pb-1 flex-row gap-1 pt-2.5 border-b border-transparent"
|
||||
>
|
||||
<woot-sidemenu-icon
|
||||
size="tiny"
|
||||
class="relative top-0 ltr:-ml-1.5 rtl:-mr-1.5"
|
||||
/>
|
||||
<router-link
|
||||
:to="searchUrl"
|
||||
class="search-link flex-1 items-center gap-1 text-left h-6 rtl:mr-3 rtl:text-right rounded-md px-2 py-0 bg-slate-25 dark:bg-slate-800 inline-flex"
|
||||
>
|
||||
<div class="flex">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
class="search--icon text-slate-800 dark:text-slate-200"
|
||||
size="16"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="search--label mb-0 overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
|
||||
</p>
|
||||
</router-link>
|
||||
<switch-layout
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggle="$emit('toggle-conversation-layout')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import SwitchLayout from './SwitchLayout.vue';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
@@ -49,7 +14,7 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mixins: [timeMixin, messageFormatterMixin],
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
@@ -66,6 +31,41 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex px-4 pb-1 flex-row gap-1 pt-2.5 border-b border-transparent"
|
||||
>
|
||||
<woot-sidemenu-icon
|
||||
size="tiny"
|
||||
class="relative top-0 ltr:-ml-1.5 rtl:-mr-1.5"
|
||||
/>
|
||||
<router-link
|
||||
:to="searchUrl"
|
||||
class="inline-flex items-center flex-1 h-6 gap-1 px-2 py-0 text-left rounded-md search-link rtl:mr-3 rtl:text-right bg-slate-25 dark:bg-slate-800"
|
||||
>
|
||||
<div class="flex">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
class="search--icon text-slate-800 dark:text-slate-200"
|
||||
size="16"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="mb-0 overflow-hidden text-sm search--label whitespace-nowrap text-ellipsis text-slate-800 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
|
||||
</p>
|
||||
</router-link>
|
||||
<SwitchLayout
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggle="$emit('toggleConversationLayout')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-link {
|
||||
&:hover {
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
<template>
|
||||
<woot-button
|
||||
v-tooltip.left="$t('CONVERSATION.SWITCH_VIEW_LAYOUT')"
|
||||
icon="arrow-right-import"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
class="layout-switch__container"
|
||||
:class="{ expanded: isOnExpandedLayout }"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
@@ -27,6 +14,19 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-button
|
||||
v-tooltip.left="$t('CONVERSATION.SWITCH_VIEW_LAYOUT')"
|
||||
icon="arrow-right-import"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
class="layout-switch__container"
|
||||
:class="{ expanded: isOnExpandedLayout }"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" soped>
|
||||
.layout-switch__container {
|
||||
&.expanded .icon {
|
||||
|
||||
@@ -1,40 +1,10 @@
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header :header-title="$t('FILTER.CUSTOM_VIEWS.ADD.TITLE')" />
|
||||
<form class="w-full" @submit.prevent="saveCustomViews">
|
||||
<div class="w-full">
|
||||
<woot-input
|
||||
v-model="name"
|
||||
:label="$t('FILTER.CUSTOM_VIEWS.ADD.LABEL')"
|
||||
type="text"
|
||||
:error="
|
||||
$v.name.$error ? $t('FILTER.CUSTOM_VIEWS.ADD.ERROR_MESSAGE') : ''
|
||||
"
|
||||
:class="{ error: $v.name.$error }"
|
||||
:placeholder="$t('FILTER.CUSTOM_VIEWS.ADD.PLACEHOLDER')"
|
||||
@blur="$v.name.$touch"
|
||||
/>
|
||||
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<woot-button :disabled="isButtonDisabled">
|
||||
{{ $t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON') }}
|
||||
</woot-button>
|
||||
<woot-button variant="clear" @click.prevent="onClose">
|
||||
{{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
filterType: {
|
||||
type: Number,
|
||||
@@ -49,7 +19,9 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
@@ -59,7 +31,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
isButtonDisabled() {
|
||||
return this.$v.name.$invalid;
|
||||
return this.v$.name.$invalid;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -75,8 +47,8 @@ export default {
|
||||
this.$emit('close');
|
||||
},
|
||||
async saveCustomViews() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -101,10 +73,40 @@ export default {
|
||||
? errorMessage
|
||||
: this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
this.openLastSavedItem();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header :header-title="$t('FILTER.CUSTOM_VIEWS.ADD.TITLE')" />
|
||||
<form class="w-full" @submit.prevent="saveCustomViews">
|
||||
<div class="w-full">
|
||||
<woot-input
|
||||
v-model="name"
|
||||
:label="$t('FILTER.CUSTOM_VIEWS.ADD.LABEL')"
|
||||
type="text"
|
||||
:error="
|
||||
v$.name.$error ? $t('FILTER.CUSTOM_VIEWS.ADD.ERROR_MESSAGE') : ''
|
||||
"
|
||||
:class="{ error: v$.name.$error }"
|
||||
:placeholder="$t('FILTER.CUSTOM_VIEWS.ADD.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<woot-button :disabled="isButtonDisabled">
|
||||
{{ $t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON') }}
|
||||
</woot-button>
|
||||
<woot-button variant="clear" @click.prevent="onClose">
|
||||
{{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<div>
|
||||
<woot-delete-modal
|
||||
v-if="showDeletePopup"
|
||||
:show.sync="showDeletePopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="deleteSavedCustomViews"
|
||||
:title="$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.TITLE')"
|
||||
:message="$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
showDeletePopup: {
|
||||
type: Boolean,
|
||||
@@ -79,7 +61,7 @@ export default {
|
||||
const filterType = this.activeCustomViews;
|
||||
await this.$store.dispatch('customViews/delete', { id, filterType });
|
||||
this.closeDeletePopup();
|
||||
this.showAlert(
|
||||
useAlert(
|
||||
this.activeFilterType === 0
|
||||
? this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_FOLDERS.SUCCESS_MESSAGE')
|
||||
: this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE')
|
||||
@@ -94,7 +76,7 @@ export default {
|
||||
: this.$t(
|
||||
'FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE'
|
||||
);
|
||||
this.showAlert(errorMessage);
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
this.openLastItemAfterDelete();
|
||||
},
|
||||
@@ -104,3 +86,20 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<div>
|
||||
<woot-delete-modal
|
||||
v-if="showDeletePopup"
|
||||
:show.sync="showDeletePopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="deleteSavedCustomViews"
|
||||
:title="$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.TITLE')"
|
||||
:message="$t('FILTER.CUSTOM_VIEWS.DELETE.MODAL.CONFIRM.MESSAGE')"
|
||||
:message-value="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { frontendURL } from '../../helper/URLHelper';
|
||||
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||
|
||||
const AppContainer = () => import('./Dashboard.vue');
|
||||
const Captain = () => import('./Captain.vue');
|
||||
const Suspended = () => import('./suspended/Index.vue');
|
||||
|
||||
export default {
|
||||
@@ -17,6 +18,14 @@ export default {
|
||||
path: frontendURL('accounts/:account_id'),
|
||||
component: AppContainer,
|
||||
children: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/captain'),
|
||||
name: 'captain',
|
||||
component: Captain,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
...inboxRoutes,
|
||||
...conversation.routes,
|
||||
...settings.routes,
|
||||
@@ -28,7 +37,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/suspended'),
|
||||
name: 'account_suspended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: Suspended,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,54 +1,15 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('HELP_CENTER.PORTAL.ADD_LOCALE.TITLE')"
|
||||
:header-content="$t('HELP_CENTER.PORTAL.ADD_LOCALE.SUB_TITLE')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="onCreate">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: $v.selectedLocale.$error }">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.LOCALE.LABEL') }}
|
||||
<select v-model="selectedLocale">
|
||||
<option
|
||||
v-for="locale in locales"
|
||||
:key="locale.name"
|
||||
:value="locale.id"
|
||||
>
|
||||
{{ locale.name }}-{{ locale.code }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="$v.selectedLocale.$error" class="message">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.LOCALE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from 'dashboard/components/Modal.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -59,6 +20,9 @@ export default {
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLocale: '',
|
||||
@@ -94,8 +58,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async onCreate() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
const updatedLocales = this.addedLocales;
|
||||
@@ -120,7 +84,7 @@ export default {
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.PORTAL.ADD_LOCALE.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
this.isUpdating = false;
|
||||
}
|
||||
},
|
||||
@@ -130,6 +94,47 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<Modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('HELP_CENTER.PORTAL.ADD_LOCALE.TITLE')"
|
||||
:header-content="$t('HELP_CENTER.PORTAL.ADD_LOCALE.SUB_TITLE')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="onCreate">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.selectedLocale.$error }">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.LOCALE.LABEL') }}
|
||||
<select v-model="selectedLocale">
|
||||
<option
|
||||
v-for="locale in locales"
|
||||
:key="locale.name"
|
||||
:value="locale.id"
|
||||
>
|
||||
{{ locale.name }}-{{ locale.code }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.selectedLocale.$error" class="message">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.LOCALE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.input-container::v-deep {
|
||||
margin: 0 0 var(--space-normal);
|
||||
|
||||
@@ -1,27 +1,3 @@
|
||||
<template>
|
||||
<div class="edit-article--container">
|
||||
<resizable-text-area
|
||||
v-model="articleTitle"
|
||||
type="text"
|
||||
:rows="1"
|
||||
class="article-heading"
|
||||
:placeholder="$t('HELP_CENTER.EDIT_ARTICLE.TITLE_PLACEHOLDER')"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onTitleInput"
|
||||
/>
|
||||
<woot-article-editor
|
||||
v-model="articleContent"
|
||||
class="article-content"
|
||||
:placeholder="$t('HELP_CENTER.EDIT_ARTICLE.CONTENT_PLACEHOLDER')"
|
||||
:enabled-menu-options="customEditorMenuOptions"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onContentInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
||||
@@ -38,10 +14,6 @@ export default {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isSettingsSidebarOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -56,7 +28,7 @@ export default {
|
||||
this.articleContent = this.article.content;
|
||||
this.saveArticle = debounce(
|
||||
values => {
|
||||
this.$emit('save-article', values);
|
||||
this.$emit('saveArticle', values);
|
||||
},
|
||||
300,
|
||||
false
|
||||
@@ -79,6 +51,30 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="edit-article--container">
|
||||
<ResizableTextArea
|
||||
v-model="articleTitle"
|
||||
type="text"
|
||||
:rows="1"
|
||||
class="article-heading"
|
||||
:placeholder="$t('HELP_CENTER.EDIT_ARTICLE.TITLE_PLACEHOLDER')"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onTitleInput"
|
||||
/>
|
||||
<WootArticleEditor
|
||||
v-model="articleContent"
|
||||
class="article-content"
|
||||
:placeholder="$t('HELP_CENTER.EDIT_ARTICLE.CONTENT_PLACEHOLDER')"
|
||||
:enabled-menu-options="customEditorMenuOptions"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onContentInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-article--container {
|
||||
@apply my-8 mx-auto py-0 max-w-[56rem] w-full;
|
||||
|
||||
@@ -1,88 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
class="text-slate-700 dark:text-slate-100 last:border-b-0 bg-white dark:bg-slate-900 my-0 -mx-4 grid grid-cols-1 lg:grid-cols-12 gap-4 border-b border-slate-50 dark:border-slate-800 px-6 py-3"
|
||||
>
|
||||
<span class="items-start flex gap-2 col-span-6 text-left">
|
||||
<fluent-icon
|
||||
v-if="showDragIcon"
|
||||
size="20"
|
||||
class="block cursor-move flex-shrink-0 h-4 mt-1 w-4 text-slate-200 dark:text-slate-700 hover:text-slate-400 hover:dark:text-slate-200"
|
||||
icon="grab-handle"
|
||||
/>
|
||||
<div class="flex flex-col truncate">
|
||||
<router-link :to="articleUrl(id)">
|
||||
<h6
|
||||
:title="title"
|
||||
class="text-base ltr:text-left rtl:text-right text-slate-800 dark:text-slate-100 mb-0.5 leading-6 font-medium hover:underline overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ title }}
|
||||
</h6>
|
||||
</router-link>
|
||||
<div class="flex gap-1 items-center">
|
||||
<Thumbnail
|
||||
v-if="author"
|
||||
:src="author.thumbnail"
|
||||
:username="author.name"
|
||||
size="14px"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-tooltip.right="
|
||||
$t('HELP_CENTER.TABLE.COLUMNS.AUTHOR_NOT_AVAILABLE')
|
||||
"
|
||||
class="flex items-center justify-center rounded w-3.5 h-3.5 bg-woot-100 dark:bg-woot-700"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="person"
|
||||
type="filled"
|
||||
size="10"
|
||||
class="text-woot-300 dark:text-woot-300"
|
||||
/>
|
||||
</div>
|
||||
<span class="font-normal text-slate-700 dark:text-slate-200 text-sm">
|
||||
{{ articleAuthorName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="flex items-center col-span-2">
|
||||
<router-link
|
||||
class="text-sm hover:underline p-0.5 truncate hover:bg-slate-25 hover:rounded-md"
|
||||
:to="getCategoryRoute(category.slug)"
|
||||
>
|
||||
<span :title="category.name">
|
||||
{{ category.name }}
|
||||
</span>
|
||||
</router-link>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center text-xs lg:text-sm"
|
||||
:title="formattedViewCount"
|
||||
>
|
||||
{{ readableViewCount }}
|
||||
<span class="lg:hidden ml-1">
|
||||
{{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="flex items-center capitalize">
|
||||
<woot-label
|
||||
class="!mb-0"
|
||||
:title="status"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
:color-scheme="labelColor"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center justify-end col-span-2 first-letter:uppercase text-slate-700 dark:text-slate-100 text-xs"
|
||||
>
|
||||
{{ lastUpdatedAt }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
@@ -91,7 +8,7 @@ export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin, portalMixin],
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
showDragIcon: {
|
||||
type: Boolean,
|
||||
@@ -103,7 +20,6 @@ export default {
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
author: {
|
||||
@@ -131,7 +47,7 @@ export default {
|
||||
|
||||
computed: {
|
||||
lastUpdatedAt() {
|
||||
return this.dynamicTime(this.updatedAt);
|
||||
return dynamicTime(this.updatedAt);
|
||||
},
|
||||
formattedViewCount() {
|
||||
return Number(this.views || 0).toLocaleString('en');
|
||||
@@ -166,3 +82,86 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 px-6 py-3 my-0 -mx-4 bg-white border-b text-slate-700 dark:text-slate-100 last:border-b-0 dark:bg-slate-900 lg:grid-cols-12 border-slate-50 dark:border-slate-800"
|
||||
>
|
||||
<span class="flex items-start col-span-6 gap-2 text-left">
|
||||
<fluent-icon
|
||||
v-if="showDragIcon"
|
||||
size="20"
|
||||
class="flex-shrink-0 block w-4 h-4 mt-1 cursor-move text-slate-200 dark:text-slate-700 hover:text-slate-400 hover:dark:text-slate-200"
|
||||
icon="grab-handle"
|
||||
/>
|
||||
<div class="flex flex-col truncate">
|
||||
<router-link :to="articleUrl(id)">
|
||||
<h6
|
||||
:title="title"
|
||||
class="text-base ltr:text-left rtl:text-right text-slate-800 dark:text-slate-100 mb-0.5 leading-6 font-medium hover:underline overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ title }}
|
||||
</h6>
|
||||
</router-link>
|
||||
<div class="flex items-center gap-1">
|
||||
<Thumbnail
|
||||
v-if="author"
|
||||
:src="author.thumbnail"
|
||||
:username="author.name"
|
||||
size="14px"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-tooltip.right="
|
||||
$t('HELP_CENTER.TABLE.COLUMNS.AUTHOR_NOT_AVAILABLE')
|
||||
"
|
||||
class="flex items-center justify-center rounded w-3.5 h-3.5 bg-woot-100 dark:bg-woot-700"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="person"
|
||||
type="filled"
|
||||
size="10"
|
||||
class="text-woot-300 dark:text-woot-300"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-normal text-slate-700 dark:text-slate-200">
|
||||
{{ articleAuthorName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="flex items-center col-span-2">
|
||||
<router-link
|
||||
class="text-sm hover:underline p-0.5 truncate hover:bg-slate-25 hover:rounded-md"
|
||||
:to="getCategoryRoute(category.slug)"
|
||||
>
|
||||
<span :title="category.name">
|
||||
{{ category.name }}
|
||||
</span>
|
||||
</router-link>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center text-xs lg:text-sm"
|
||||
:title="formattedViewCount"
|
||||
>
|
||||
{{ readableViewCount }}
|
||||
<span class="ml-1 lg:hidden">
|
||||
{{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="flex items-center capitalize">
|
||||
<woot-label
|
||||
class="!mb-0"
|
||||
:title="status"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
:color-scheme="labelColor"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center justify-end col-span-2 text-xs first-letter:uppercase text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
{{ lastUpdatedAt }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+49
-54
@@ -1,17 +1,63 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
|
||||
export default {
|
||||
name: 'ArticleSearchResultItem',
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Untitled',
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleInsert(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('insert', this.id);
|
||||
},
|
||||
handlePreview(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('preview', this.id);
|
||||
},
|
||||
async handleCopy(e) {
|
||||
e.stopPropagation();
|
||||
await copyTextToClipboard(this.url);
|
||||
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex flex-col gap-1 bg-white dark:bg-slate-900 hover:bg-slate-25 hover:dark:bg-slate-800 rounded-md py-1 px-2 w-full group border border-transparent border-solid focus:outline-none focus:bg-slate-25 focus:border-slate-500 dark:focus:border-slate-400 dark:focus:bg-slate-800 cursor-pointer"
|
||||
class="flex flex-col w-full gap-1 px-2 py-1 bg-white border border-transparent border-solid rounded-md cursor-pointer dark:bg-slate-900 hover:bg-slate-25 hover:dark:bg-slate-800 group focus:outline-none focus:bg-slate-25 focus:border-slate-500 dark:focus:border-slate-400 dark:focus:bg-slate-800"
|
||||
@click="handlePreview"
|
||||
>
|
||||
<h4
|
||||
class="text-sm ltr:text-left rtl:text-right w-full mb-0 text-slate-900 dark:text-slate-25 -mx-1 rounded-sm hover:underline group-hover:underline"
|
||||
class="w-full mb-0 -mx-1 text-sm rounded-sm ltr:text-left rtl:text-right text-slate-900 dark:text-slate-25 hover:underline group-hover:underline"
|
||||
>
|
||||
{{ title }}
|
||||
</h4>
|
||||
|
||||
<div class="flex content-between items-center gap-0.5 w-full">
|
||||
<p
|
||||
class="text-sm ltr:text-left rtl:text-right text-slate-600 dark:text-slate-300 mb-0 w-full"
|
||||
class="w-full mb-0 text-sm ltr:text-left rtl:text-right text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ locale }}
|
||||
{{ ` / ` }}
|
||||
@@ -40,54 +86,3 @@
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
name: 'ArticleSearchResultItem',
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Untitled',
|
||||
},
|
||||
body: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleInsert(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('insert', this.id);
|
||||
},
|
||||
handlePreview(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('preview', this.id);
|
||||
},
|
||||
async handleCopy(e) {
|
||||
e.stopPropagation();
|
||||
await copyTextToClipboard(this.url);
|
||||
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+41
-41
@@ -1,44 +1,3 @@
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col flex-1 overflow-hidden">
|
||||
<div class="py-1">
|
||||
<woot-button
|
||||
variant="link"
|
||||
size="small"
|
||||
icon="chevron-left"
|
||||
@click="onBack"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.BACK_RESULTS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="-ml-4 h-full overflow-y-auto">
|
||||
<div class="w-full h-full min-h-0">
|
||||
<iframe-loader :url="url" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 py-2">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
is-expanded
|
||||
color-scheme="secondary"
|
||||
icon="chevron-left"
|
||||
@click="onBack"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.BACK') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="small"
|
||||
is-expanded
|
||||
icon="arrow-download"
|
||||
@click="onInsert"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.INSERT_ARTICLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IframeLoader from 'shared/components/IframeLoader.vue';
|
||||
|
||||
@@ -65,3 +24,44 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col flex-1 overflow-hidden">
|
||||
<div class="py-1">
|
||||
<woot-button
|
||||
variant="link"
|
||||
size="small"
|
||||
icon="chevron-left"
|
||||
@click="onBack"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.BACK_RESULTS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="-ml-4 h-full overflow-y-auto">
|
||||
<div class="w-full h-full min-h-0">
|
||||
<IframeLoader :url="url" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 py-2">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
is-expanded
|
||||
color-scheme="secondary"
|
||||
icon="chevron-left"
|
||||
@click="onBack"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.BACK') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="small"
|
||||
is-expanded
|
||||
icon="arrow-download"
|
||||
@click="onInsert"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.INSERT_ARTICLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+48
-52
@@ -1,5 +1,51 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Chatwoot',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['search', 'close']);
|
||||
|
||||
const articleSearchHeaderRef = ref(null);
|
||||
const searchInputRef = ref(null);
|
||||
const searchQuery = ref('');
|
||||
|
||||
onMounted(() => {
|
||||
searchInputRef.value.focus();
|
||||
});
|
||||
|
||||
const onInput = e => {
|
||||
emit('search', e.target.value);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
Slash: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
searchInputRef.value.focus();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => {
|
||||
onClose();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents, articleSearchHeaderRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col py-1">
|
||||
<div ref="articleSearchHeaderRef" class="flex flex-col py-1">
|
||||
<div class="flex items-center justify-between py-2 mb-1">
|
||||
<h3 class="text-base text-slate-900 dark:text-slate-25">
|
||||
{{ title }}
|
||||
@@ -20,63 +66,13 @@
|
||||
<fluent-icon icon="search" class="" size="18" />
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
ref="searchInputRef"
|
||||
type="text"
|
||||
:placeholder="$t('HELP_CENTER.ARTICLE_SEARCH.PLACEHOLDER')"
|
||||
class="block w-full !h-9 ltr:!pl-8 rtl:!pr-8 dark:!bg-slate-700 !bg-slate-25 text-sm rounded-md leading-8 text-slate-700 shadow-sm ring-2 ring-transparent ring-slate-300 border border-solid border-slate-300 placeholder:text-slate-400 focus:border-woot-600 focus:ring-woot-200 !mb-0 focus:bg-slate-25 dark:focus:bg-slate-700 dark:focus:ring-woot-700"
|
||||
:value="searchQuery"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
name: 'ChatwootSearch',
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Chatwoot',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
isInputFocused: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
this.$emit('search', e.target.value);
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
onFocus() {
|
||||
this.isInputFocused = true;
|
||||
},
|
||||
onBlur() {
|
||||
this.isInputFocused = false;
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Slash: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+37
-50
@@ -1,40 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed flex items-center justify-center w-screen h-screen bg-modal-backdrop-light dark:bg-modal-backdrop-dark top-0 left-0 z-50"
|
||||
>
|
||||
<div
|
||||
v-on-clickaway="onClose"
|
||||
class="flex flex-col px-4 pb-4 rounded-md shadow-md border border-solid border-slate-50 dark:border-slate-800 bg-white dark:bg-slate-900 z-[1000] max-w-[720px] md:w-[20rem] lg:w-[24rem] xl:w-[28rem] 2xl:w-[32rem] h-[calc(100vh-20rem)] max-h-[40rem]"
|
||||
>
|
||||
<search-header
|
||||
:title="$t('HELP_CENTER.ARTICLE_SEARCH.TITLE')"
|
||||
class="w-full sticky top-0 bg-[inherit]"
|
||||
@close="onClose"
|
||||
@search="onSearch"
|
||||
/>
|
||||
|
||||
<article-view
|
||||
v-if="activeId"
|
||||
:url="articleViewerUrl"
|
||||
@back="onBack"
|
||||
@insert="onInsert"
|
||||
/>
|
||||
<search-results
|
||||
v-else
|
||||
:search-query="searchQuery"
|
||||
:is-loading="isLoading"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
:articles="searchResultsWithUrl"
|
||||
@preview="handlePreview"
|
||||
@insert="onInsert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import SearchHeader from './Header.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
@@ -42,7 +8,6 @@ import ArticleView from './ArticleView.vue';
|
||||
import ArticlesAPI from 'dashboard/api/helpCenter/articles';
|
||||
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
|
||||
import portalMixin from '../../mixins/portalMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
name: 'ArticleSearchPopover',
|
||||
@@ -51,7 +16,7 @@ export default {
|
||||
SearchResults,
|
||||
ArticleView,
|
||||
},
|
||||
mixins: [portalMixin, alertMixin, keyboardEventListenerMixins],
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
selectedPortalSlug: {
|
||||
type: String,
|
||||
@@ -145,21 +110,43 @@ export default {
|
||||
const article = this.activeArticle(id || this.activeId);
|
||||
|
||||
this.$emit('insert', article);
|
||||
this.showAlert(
|
||||
this.$t('HELP_CENTER.ARTICLE_SEARCH.SUCCESS_ARTICLE_INSERTED')
|
||||
);
|
||||
useAlert(this.$t('HELP_CENTER.ARTICLE_SEARCH.SUCCESS_ARTICLE_INSERTED'));
|
||||
this.onClose();
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
Escape: {
|
||||
action: () => {
|
||||
this.onClose();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="fixed top-0 left-0 z-50 flex items-center justify-center w-screen h-screen bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
|
||||
>
|
||||
<div
|
||||
v-on-clickaway="onClose"
|
||||
class="flex flex-col px-4 pb-4 rounded-md shadow-md border border-solid border-slate-50 dark:border-slate-800 bg-white dark:bg-slate-900 z-[1000] max-w-[720px] md:w-[20rem] lg:w-[24rem] xl:w-[28rem] 2xl:w-[32rem] h-[calc(100vh-20rem)] max-h-[40rem]"
|
||||
>
|
||||
<SearchHeader
|
||||
:title="$t('HELP_CENTER.ARTICLE_SEARCH.TITLE')"
|
||||
class="w-full sticky top-0 bg-[inherit]"
|
||||
@close="onClose"
|
||||
@search="onSearch"
|
||||
/>
|
||||
|
||||
<ArticleView
|
||||
v-if="activeId"
|
||||
:url="articleViewerUrl"
|
||||
@back="onBack"
|
||||
@insert="onInsert"
|
||||
/>
|
||||
<SearchResults
|
||||
v-else
|
||||
:search-query="searchQuery"
|
||||
:is-loading="isLoading"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
:articles="searchResultsWithUrl"
|
||||
@preview="handlePreview"
|
||||
@insert="onInsert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+30
-32
@@ -1,31 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-end gap-1 py-4 bg-white dark:bg-slate-900 h-full overflow-y-auto"
|
||||
>
|
||||
<div class="flex flex-col gap-1 w-full">
|
||||
<div v-if="isLoading" class="empty-state-message">
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.SEARCH_LOADER') }}
|
||||
</div>
|
||||
<div v-else-if="showNoResults" class="empty-state-message">
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.NO_RESULT') }}
|
||||
</div>
|
||||
<search-result-item
|
||||
v-for="article in articles"
|
||||
v-else
|
||||
:id="article.id"
|
||||
:key="article.id"
|
||||
:title="article.title"
|
||||
:body="article.content"
|
||||
:url="article.url"
|
||||
:category="article.category.name"
|
||||
:locale="article.localeName"
|
||||
@preview="handlePreview"
|
||||
@insert="handleInsert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchResultItem from './ArticleSearchResultItem.vue';
|
||||
|
||||
@@ -47,10 +19,6 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
portalSlug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showNoResults() {
|
||||
@@ -67,6 +35,36 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-end h-full gap-1 py-4 overflow-y-auto bg-white dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
<div v-if="isLoading" class="empty-state-message">
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.SEARCH_LOADER') }}
|
||||
</div>
|
||||
<div v-else-if="showNoResults" class="empty-state-message">
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.NO_RESULT') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<SearchResultItem
|
||||
v-for="article in articles"
|
||||
:id="article.id"
|
||||
:key="article.id"
|
||||
:title="article.title"
|
||||
:body="article.content"
|
||||
:url="article.url"
|
||||
:category="article.category.name"
|
||||
:locale="article.localeName"
|
||||
@preview="handlePreview"
|
||||
@insert="handleInsert"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.empty-state-message {
|
||||
@apply text-center flex items-center justify-center px-4 py-8 text-slate-500 text-sm;
|
||||
|
||||
@@ -1,70 +1,3 @@
|
||||
<template>
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="hidden lg:grid py-0 px-6 h-12 content-center grid-cols-12 z-10 gap-4 border-b border-slate-50 dark:border-slate-700 sticky top-16 bg-white dark:bg-slate-900"
|
||||
:class="{ draggable: onCategoryPage }"
|
||||
>
|
||||
<div
|
||||
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-left rtl:text-right col-span-6"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.TITLE') }}
|
||||
</div>
|
||||
<div
|
||||
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-left rtl:text-right col-span-2"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.CATEGORY') }}
|
||||
</div>
|
||||
<div
|
||||
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-left rtl:text-right hidden lg:block"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.READ_COUNT') }}
|
||||
</div>
|
||||
<div
|
||||
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-left rtl:text-right"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.STATUS') }}
|
||||
</div>
|
||||
<div
|
||||
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-right rtl:text-left hidden md:block col-span-2"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.LAST_EDITED') }}
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
tag="div"
|
||||
class="border-t-0 px-4 pb-4"
|
||||
:disabled="!dragEnabled"
|
||||
:list="localArticles"
|
||||
ghost-class="article-ghost-class"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<ArticleItem
|
||||
v-for="article in localArticles"
|
||||
:id="article.id"
|
||||
:key="article.id"
|
||||
:class="{ draggable: onCategoryPage }"
|
||||
:title="article.title"
|
||||
:author="article.author"
|
||||
:show-drag-icon="dragEnabled"
|
||||
:category="article.category"
|
||||
:views="article.views"
|
||||
:status="article.status"
|
||||
:updated-at="article.updated_at"
|
||||
/>
|
||||
</draggable>
|
||||
|
||||
<table-footer
|
||||
v-if="showArticleFooter"
|
||||
:current-page="currentPage"
|
||||
:total-count="totalCount"
|
||||
:page-size="pageSize"
|
||||
class="dark:bg-slate-900 bottom-0 border-t border-slate-75 dark:border-slate-700/50"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ArticleItem from './ArticleItem.vue';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||
@@ -74,7 +7,7 @@ export default {
|
||||
components: {
|
||||
ArticleItem,
|
||||
TableFooter,
|
||||
draggable,
|
||||
Draggable: draggable,
|
||||
},
|
||||
props: {
|
||||
articles: {
|
||||
@@ -150,11 +83,79 @@ export default {
|
||||
this.$emit('reorder', reorderedGroup);
|
||||
},
|
||||
onPageChange(page) {
|
||||
this.$emit('page-change', page);
|
||||
this.$emit('pageChange', page);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="sticky z-10 content-center hidden h-12 grid-cols-12 gap-4 px-6 py-0 bg-white border-b lg:grid border-slate-50 dark:border-slate-700 top-16 dark:bg-slate-900"
|
||||
:class="{ draggable: onCategoryPage }"
|
||||
>
|
||||
<div
|
||||
class="col-span-6 px-0 py-2 text-sm font-semibold text-left capitalize text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.TITLE') }}
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 px-0 py-2 text-sm font-semibold text-left capitalize text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.CATEGORY') }}
|
||||
</div>
|
||||
<div
|
||||
class="hidden px-0 py-2 text-sm font-semibold text-left capitalize text-slate-700 dark:text-slate-100 rtl:text-right lg:block"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.READ_COUNT') }}
|
||||
</div>
|
||||
<div
|
||||
class="px-0 py-2 text-sm font-semibold text-left capitalize text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.STATUS') }}
|
||||
</div>
|
||||
<div
|
||||
class="hidden col-span-2 px-0 py-2 text-sm font-semibold text-right capitalize text-slate-700 dark:text-slate-100 rtl:text-left md:block"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.LAST_EDITED') }}
|
||||
</div>
|
||||
</div>
|
||||
<Draggable
|
||||
tag="div"
|
||||
class="px-4 pb-4 border-t-0"
|
||||
:disabled="!dragEnabled"
|
||||
:list="localArticles"
|
||||
ghost-class="article-ghost-class"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<ArticleItem
|
||||
v-for="article in localArticles"
|
||||
:id="article.id"
|
||||
:key="article.id"
|
||||
:class="{ draggable: onCategoryPage }"
|
||||
:title="article.title"
|
||||
:author="article.author"
|
||||
:show-drag-icon="dragEnabled"
|
||||
:category="article.category"
|
||||
:views="article.views"
|
||||
:status="article.status"
|
||||
:updated-at="article.updated_at"
|
||||
/>
|
||||
</Draggable>
|
||||
|
||||
<TableFooter
|
||||
v-if="showArticleFooter"
|
||||
:current-page="currentPage"
|
||||
:total-count="totalCount"
|
||||
:page-size="pageSize"
|
||||
class="bottom-0 border-t dark:bg-slate-900 border-slate-75 dark:border-slate-700/50"
|
||||
@pageChange="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/*
|
||||
The .article-ghost-class class is maintained as the vueDraggable doesn't allow multiple classes
|
||||
|
||||
+141
-141
@@ -1,142 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex p-6 items-center justify-between w-full h-16 sticky top-0 z-50 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<woot-sidemenu-icon />
|
||||
<div class="flex items-center my-0 mx-2">
|
||||
<h3 class="text-xl text-slate-800 dark:text-slate-100 font-medium mb-0">
|
||||
{{ headerTitle }}
|
||||
</h3>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300 mx-2 mt-0.5">{{
|
||||
`(${count})`
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
icon="filter"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="openFilterModal"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.FILTER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
icon="arrow-sort"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
variant="hollow"
|
||||
@click="openDropdown"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.SORT') }}
|
||||
<span
|
||||
class="inline-flex ml-1 rtl:ml-0 rtl:mr-1 items-center text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ selectedValue }}
|
||||
<Fluent-icon class="dropdown-arrow" icon="chevron-down" size="14" />
|
||||
</span>
|
||||
</woot-button>
|
||||
<div
|
||||
v-if="showSortByDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="send-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.PUBLISHED') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="dual-screen-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.DRAFT') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="calendar-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.ARCHIVED') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.HEADER.SETTINGS_BUTTON')"
|
||||
icon="settings"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
<div class="relative">
|
||||
<woot-button
|
||||
v-if="shouldShowLocaleDropdown"
|
||||
icon="globe"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
variant="hollow"
|
||||
@click="openLocaleDropdown"
|
||||
>
|
||||
<div class="flex justify-between w-full min-w-0 items-center">
|
||||
<span
|
||||
class="inline-flex ml-1 rtl:ml-0 rtl:mr-1 items-center text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ selectedLocale }}
|
||||
<Fluent-icon
|
||||
class="dropdown-arrow"
|
||||
icon="chevron-down"
|
||||
size="14"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</woot-button>
|
||||
<div
|
||||
v-if="showLocaleDropdown"
|
||||
v-on-clickaway="closeLocaleDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<multiselect-dropdown-items
|
||||
:options="switchableLocales"
|
||||
:has-thumbnail="false"
|
||||
:selected-items="[selectedLocale]"
|
||||
:input-placeholder="
|
||||
$t('HELP_CENTER.HEADER.LOCALE_SELECT.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="$t('HELP_CENTER.HEADER.LOCALE_SELECT.NO_RESULT')"
|
||||
@click="onClickSelectItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="add"
|
||||
color-scheme="primary"
|
||||
@click="onClickNewArticlePage"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.NEW_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
@@ -211,7 +72,7 @@ export default {
|
||||
this.showLocaleDropdown = false;
|
||||
},
|
||||
onClickNewArticlePage() {
|
||||
this.$emit('new-article-page');
|
||||
this.$emit('newArticlePage');
|
||||
},
|
||||
onClickSelectItem(value) {
|
||||
const { name, code } = value;
|
||||
@@ -219,12 +80,151 @@ export default {
|
||||
if (!name || name === this.selectedLocale) {
|
||||
return;
|
||||
}
|
||||
this.$emit('change-locale', code);
|
||||
this.$emit('changeLocale', code);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="sticky top-0 z-50 flex items-center justify-between w-full h-16 p-6 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<woot-sidemenu-icon />
|
||||
<div class="flex items-center mx-2 my-0">
|
||||
<h3 class="mb-0 text-xl font-medium text-slate-800 dark:text-slate-100">
|
||||
{{ headerTitle }}
|
||||
</h3>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300 mx-2 mt-0.5">{{
|
||||
`(${count})`
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
icon="filter"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="openFilterModal"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.FILTER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
icon="arrow-sort"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
variant="hollow"
|
||||
@click="openDropdown"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.SORT') }}
|
||||
<span
|
||||
class="inline-flex items-center ml-1 rtl:ml-0 rtl:mr-1 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ selectedValue }}
|
||||
<FluentIcon class="dropdown-arrow" icon="chevron-down" size="14" />
|
||||
</span>
|
||||
</woot-button>
|
||||
<div
|
||||
v-if="showSortByDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<WootDropdownMenu>
|
||||
<WootDropdownItem>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="send-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.PUBLISHED') }}
|
||||
</woot-button>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="dual-screen-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.DRAFT') }}
|
||||
</woot-button>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="calendar-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.ARCHIVED') }}
|
||||
</woot-button>
|
||||
</WootDropdownItem>
|
||||
</WootDropdownMenu>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.HEADER.SETTINGS_BUTTON')"
|
||||
icon="settings"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
<div class="relative">
|
||||
<woot-button
|
||||
v-if="shouldShowLocaleDropdown"
|
||||
icon="globe"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
variant="hollow"
|
||||
@click="openLocaleDropdown"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full min-w-0">
|
||||
<span
|
||||
class="inline-flex items-center ml-1 rtl:ml-0 rtl:mr-1 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ selectedLocale }}
|
||||
<FluentIcon
|
||||
class="dropdown-arrow"
|
||||
icon="chevron-down"
|
||||
size="14"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</woot-button>
|
||||
<div
|
||||
v-if="showLocaleDropdown"
|
||||
v-on-clickaway="closeLocaleDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<MultiselectDropdownItems
|
||||
:options="switchableLocales"
|
||||
:has-thumbnail="false"
|
||||
:selected-items="[selectedLocale]"
|
||||
:input-placeholder="
|
||||
$t('HELP_CENTER.HEADER.LOCALE_SELECT.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="$t('HELP_CENTER.HEADER.LOCALE_SELECT.NO_RESULT')"
|
||||
@click="onClickSelectItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="add"
|
||||
color-scheme="primary"
|
||||
@click="onClickNewArticlePage"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.NEW_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dropdown-pane--open {
|
||||
@apply absolute top-10 right-0 z-50 min-w-[8rem];
|
||||
|
||||
+111
-117
@@ -1,121 +1,11 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full h-16">
|
||||
<div class="flex items-center">
|
||||
<woot-button
|
||||
icon="chevron-left"
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="primary"
|
||||
class="back-button"
|
||||
@click="onClickGoBack"
|
||||
>
|
||||
{{ backButtonLabel }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
v-if="isUpdating || isSaved"
|
||||
class="draft-status mr-1 ml-4 rtl:ml-2 rtl:mr-4 text-slate-400 dark:text-slate-300 items-center text-xs"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
|
||||
<woot-button
|
||||
class-names="article--buttons relative"
|
||||
icon="globe"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="showPreview"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.PREVIEW') }}
|
||||
</woot-button>
|
||||
<!-- Hidden since this is in V2
|
||||
<woot-button
|
||||
v-if="shouldShowAddLocaleButton"
|
||||
class-names="article--buttons relative"
|
||||
icon="add"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="onClickAdd"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.ADD_TRANSLATION') }}
|
||||
</woot-button> -->
|
||||
<woot-button
|
||||
v-if="!isSidebarOpen"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.OPEN_SIDEBAR')"
|
||||
icon="pane-open"
|
||||
class-names="article--buttons relative sidebar-button"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
:is-disabled="enableOpenSidebarButton"
|
||||
@click="openSidebar"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="isSidebarOpen"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.CLOSE_SIDEBAR')"
|
||||
icon="pane-close"
|
||||
class-names="article--buttons relative"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
@click="closeSidebar"
|
||||
/>
|
||||
<div class="article--buttons relative">
|
||||
<div class="button-group">
|
||||
<woot-button
|
||||
class-names="publish-button"
|
||||
size="small"
|
||||
icon="checkmark"
|
||||
color-scheme="primary"
|
||||
:is-disabled="!articleSlug || isPublishedArticle"
|
||||
@click="updateArticleStatus(ARTICLE_STATUS_TYPES.PUBLISH)"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.PUBLISH_BUTTON') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
ref="arrowDownButton"
|
||||
size="small"
|
||||
icon="chevron-down"
|
||||
:is-disabled="!articleSlug || isArchivedArticle"
|
||||
@click="openActionsDropdown"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeActionsDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="book-clock"
|
||||
@click="updateArticleStatus(ARTICLE_STATUS_TYPES.ARCHIVE)"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.MOVE_TO_ARCHIVE_BUTTON') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
const { ARTICLE_STATUS_TYPES } = wootConstants;
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
isSidebarOpen: {
|
||||
type: Boolean,
|
||||
@@ -137,10 +27,6 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shouldShowAddLocaleButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -188,7 +74,7 @@ export default {
|
||||
articleId: this.articleSlug,
|
||||
status: status,
|
||||
});
|
||||
this.$emit('update-meta');
|
||||
this.$emit('updateMeta');
|
||||
this.statusUpdateSuccessMessage(status);
|
||||
this.closeActionsDropdown();
|
||||
if (status === this.ARTICLE_STATUS_TYPES.ARCHIVE) {
|
||||
@@ -200,7 +86,7 @@ export default {
|
||||
this.alertMessage =
|
||||
error?.message || this.statusUpdateErrorMessage(status);
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
statusUpdateSuccessMessage(status) {
|
||||
@@ -233,6 +119,114 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full h-16">
|
||||
<div class="flex items-center">
|
||||
<woot-button
|
||||
icon="chevron-left"
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="primary"
|
||||
class="back-button"
|
||||
@click="onClickGoBack"
|
||||
>
|
||||
{{ backButtonLabel }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
v-if="isUpdating || isSaved"
|
||||
class="items-center ml-4 mr-1 text-xs draft-status rtl:ml-2 rtl:mr-4 text-slate-400 dark:text-slate-300"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
|
||||
<woot-button
|
||||
class-names="article--buttons relative"
|
||||
icon="globe"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="showPreview"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.PREVIEW') }}
|
||||
</woot-button>
|
||||
<!-- Hidden since this is in V2
|
||||
<woot-button
|
||||
v-if="shouldShowAddLocaleButton"
|
||||
class-names="article--buttons relative"
|
||||
icon="add"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="onClickAdd"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.ADD_TRANSLATION') }}
|
||||
</woot-button> -->
|
||||
<woot-button
|
||||
v-if="!isSidebarOpen"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.OPEN_SIDEBAR')"
|
||||
icon="pane-open"
|
||||
class-names="article--buttons relative sidebar-button"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
:is-disabled="enableOpenSidebarButton"
|
||||
@click="openSidebar"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="isSidebarOpen"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.CLOSE_SIDEBAR')"
|
||||
icon="pane-close"
|
||||
class-names="article--buttons relative"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
@click="closeSidebar"
|
||||
/>
|
||||
<div class="relative article--buttons">
|
||||
<div class="button-group">
|
||||
<woot-button
|
||||
class-names="publish-button"
|
||||
size="small"
|
||||
icon="checkmark"
|
||||
color-scheme="primary"
|
||||
:is-disabled="!articleSlug || isPublishedArticle"
|
||||
@click="updateArticleStatus(ARTICLE_STATUS_TYPES.PUBLISH)"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.PUBLISH_BUTTON') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="chevron-down"
|
||||
:is-disabled="!articleSlug || isArchivedArticle"
|
||||
@click="openActionsDropdown"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeActionsDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="book-clock"
|
||||
@click="updateArticleStatus(ARTICLE_STATUS_TYPES.ARCHIVE)"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.MOVE_TO_ARCHIVE_BUTTON') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article--buttons {
|
||||
.dropdown-pane {
|
||||
|
||||
+73
-64
@@ -1,78 +1,20 @@
|
||||
<template>
|
||||
<div class="app-wrapper flex h-full flex-grow-0 min-h-0 w-full">
|
||||
<sidebar
|
||||
:route="currentRoute"
|
||||
@toggle-account-modal="toggleAccountModal"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
/>
|
||||
<help-center-sidebar
|
||||
v-if="showHelpCenterSidebar"
|
||||
:header-title="headerTitle"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
:locale-slug="selectedLocaleInPortal"
|
||||
:sub-title="localeName(selectedLocaleInPortal)"
|
||||
:accessible-menu-items="accessibleMenuItems"
|
||||
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
||||
@open-popover="openPortalPopover"
|
||||
@open-modal="onClickOpenAddCategoryModal"
|
||||
/>
|
||||
<section
|
||||
v-if="isHelpCenterEnabled"
|
||||
class="flex h-full min-h-0 overflow-hidden flex-1 px-0 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<router-view @reload-locale="fetchPortalAndItsCategories" />
|
||||
<command-bar />
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
/>
|
||||
<woot-key-shortcut-modal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<notification-panel
|
||||
v-if="showNotificationPanel"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<portal-popover
|
||||
v-if="showPortalPopover"
|
||||
:portals="portals"
|
||||
:active-portal-slug="selectedPortalSlug"
|
||||
:active-locale="selectedLocaleInPortal"
|
||||
@fetch-portal="fetchPortalAndItsCategories"
|
||||
@close-popover="closePortalPopover"
|
||||
/>
|
||||
<add-category
|
||||
v-if="showAddCategoryModal"
|
||||
:show.sync="showAddCategoryModal"
|
||||
:portal-name="selectedPortalName"
|
||||
:locale="selectedLocaleInPortal"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
@cancel="onClickCloseAddCategoryModal"
|
||||
/>
|
||||
</section>
|
||||
<upgrade-page v-else />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import UpgradePage from './UpgradePage';
|
||||
import UpgradePage from './UpgradePage.vue';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import Sidebar from 'dashboard/components/layout/Sidebar.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import PortalPopover from '../components/PortalPopover.vue';
|
||||
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
|
||||
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
|
||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
|
||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
import AddCategory from '../pages/categories/AddCategory.vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
const CommandBar = () =>
|
||||
import('dashboard/routes/dashboard/commands/commandbar.vue');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -86,7 +28,15 @@ export default {
|
||||
UpgradePage,
|
||||
WootKeyShortcutModal,
|
||||
},
|
||||
mixins: [portalMixin, uiSettingsMixin],
|
||||
mixins: [portalMixin],
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOnDesktop: true,
|
||||
@@ -105,7 +55,6 @@ export default {
|
||||
portals: 'portals/allPortals',
|
||||
categories: 'categories/allCategories',
|
||||
meta: 'portals/getMeta',
|
||||
isFetching: 'portals/isFetchingPortals',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
|
||||
@@ -333,3 +282,63 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-grow-0 w-full h-full min-h-0 app-wrapper">
|
||||
<Sidebar
|
||||
:route="currentRoute"
|
||||
@toggleAccountModal="toggleAccountModal"
|
||||
@openNotificationPanel="openNotificationPanel"
|
||||
@openKeyShortcutModal="toggleKeyShortcutModal"
|
||||
@closeKeyShortcutModal="closeKeyShortcutModal"
|
||||
/>
|
||||
<HelpCenterSidebar
|
||||
v-if="showHelpCenterSidebar"
|
||||
:header-title="headerTitle"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
:locale-slug="selectedLocaleInPortal"
|
||||
:sub-title="localeName(selectedLocaleInPortal)"
|
||||
:accessible-menu-items="accessibleMenuItems"
|
||||
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
||||
@openPopover="openPortalPopover"
|
||||
@openModal="onClickOpenAddCategoryModal"
|
||||
/>
|
||||
<section
|
||||
v-if="isHelpCenterEnabled"
|
||||
class="flex flex-1 h-full min-h-0 px-0 overflow-hidden bg-white dark:bg-slate-900"
|
||||
>
|
||||
<router-view @reloadLocale="fetchPortalAndItsCategories" />
|
||||
<CommandBar />
|
||||
<AccountSelector
|
||||
:show-account-modal="showAccountModal"
|
||||
@closeAccountModal="toggleAccountModal"
|
||||
/>
|
||||
<WootKeyShortcutModal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<NotificationPanel
|
||||
v-if="showNotificationPanel"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<PortalPopover
|
||||
v-if="showPortalPopover"
|
||||
:portals="portals"
|
||||
:active-portal-slug="selectedPortalSlug"
|
||||
:active-locale="selectedLocaleInPortal"
|
||||
@fetchPortal="fetchPortalAndItsCategories"
|
||||
@closePopover="closePortalPopover"
|
||||
/>
|
||||
<AddCategory
|
||||
v-if="showAddCategoryModal"
|
||||
:show.sync="showAddCategoryModal"
|
||||
:portal-name="selectedPortalName"
|
||||
:locale="selectedLocaleInPortal"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
@cancel="onClickCloseAddCategoryModal"
|
||||
/>
|
||||
</section>
|
||||
<UpgradePage v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+205
-199
@@ -1,207 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 rounded-md relative flex mb-3 p-4 border border-solid border-slate-100 dark:border-slate-600"
|
||||
>
|
||||
<thumbnail :username="portal.name" variant="square" />
|
||||
<div class="ml-2 rtl:ml-0 rtl:mr-2 flex-grow">
|
||||
<header class="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<h2 class="mb-0 text-lg text-slate-800 dark:text-slate-100">
|
||||
{{ portal.name }}
|
||||
</h2>
|
||||
<woot-label
|
||||
:title="status"
|
||||
:color-scheme="labelColor"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
class="my-0 mx-2"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-sm mb-0 text-slate-700 dark:text-slate-200">
|
||||
{{ articleCount }}
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.COUNT_LABEL'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row gap-1">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="primary"
|
||||
@click="addLocale"
|
||||
>
|
||||
{{
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.ADD')
|
||||
}}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
@click="openSite"
|
||||
>
|
||||
{{
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.VISIT')
|
||||
}}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.SETTINGS'
|
||||
)
|
||||
"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
icon="settings"
|
||||
color-scheme="secondary"
|
||||
@click="openSettings"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.DELETE')
|
||||
"
|
||||
variant="hollow"
|
||||
color-scheme="alert"
|
||||
size="small"
|
||||
icon="delete"
|
||||
@click="onClickOpenDeleteModal(portal)"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mb-12">
|
||||
<h2
|
||||
class="text-slate-800 dark:text-slate-100 font-medium mb-2 text-base"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.TITLE'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<div
|
||||
class="flex justify-between mr-[6.25rem] rtl:mr-0 rtl:ml-[6.25rem] max-w-[80vw]"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-start flex-col mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.NAME'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-start flex-col mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.DOMAIN'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.custom_domain }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-start flex-col mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SLUG'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.slug }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-start flex-col mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.TITLE'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.page_title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-start flex-col mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.THEME'
|
||||
)
|
||||
}}</label>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-4 h-4 rounded-md mr-1 rtl:mr-0 rtl:ml-1 border border-solid border-slate-25 dark:border-slate-800"
|
||||
:style="{ background: portal.color }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start flex-col mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SUB_TEXT'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.header_text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-12">
|
||||
<h2
|
||||
class="text-slate-800 dark:text-slate-100 font-medium mb-2 text-base"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TITLE'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<locale-item-table
|
||||
:locales="locales"
|
||||
:selected-locale-code="portal.meta.default_locale"
|
||||
@change-default-locale="changeDefaultLocale"
|
||||
@delete="deletePortalLocale"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="onClickDeletePortal"
|
||||
:title="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.TITLE')"
|
||||
:message="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.MESSAGE')"
|
||||
:message-value="deleteMessageValue"
|
||||
:confirm-text="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.YES')"
|
||||
:reject-text="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import LocaleItemTable from './PortalListItemTable.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
thumbnail,
|
||||
Thumbnail: thumbnail,
|
||||
LocaleItemTable,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
@@ -213,6 +21,13 @@ export default {
|
||||
values: ['archived', 'draft', 'published'],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
@@ -249,10 +64,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
addLocale() {
|
||||
this.$emit('add-locale', this.portal.id);
|
||||
this.$emit('addLocale', this.portal.id);
|
||||
},
|
||||
openSite() {
|
||||
this.$emit('open-site', this.portal.slug);
|
||||
this.$emit('openSite', this.portal.slug);
|
||||
},
|
||||
openSettings() {
|
||||
this.fetchPortalAndItsCategories();
|
||||
@@ -300,7 +115,7 @@ export default {
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_ERROR'
|
||||
);
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
changeDefaultLocale({ localeCode }) {
|
||||
@@ -357,7 +172,7 @@ export default {
|
||||
} catch (error) {
|
||||
this.alertMessage = error?.message || errorMessage;
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
navigateToPortalEdit() {
|
||||
@@ -369,3 +184,194 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="relative flex p-4 mb-3 bg-white border border-solid rounded-md dark:bg-slate-900 border-slate-100 dark:border-slate-600"
|
||||
>
|
||||
<Thumbnail :username="portal.name" variant="square" />
|
||||
<div class="flex-grow ml-2 rtl:ml-0 rtl:mr-2">
|
||||
<header class="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<h2 class="mb-0 text-lg text-slate-800 dark:text-slate-100">
|
||||
{{ portal.name }}
|
||||
</h2>
|
||||
<woot-label
|
||||
:title="status"
|
||||
:color-scheme="labelColor"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
class="mx-2 my-0"
|
||||
/>
|
||||
</div>
|
||||
<p class="mb-0 text-sm text-slate-700 dark:text-slate-200">
|
||||
{{ articleCount }}
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.COUNT_LABEL'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row gap-1">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="primary"
|
||||
@click="addLocale"
|
||||
>
|
||||
{{
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.ADD')
|
||||
}}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
@click="openSite"
|
||||
>
|
||||
{{
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.VISIT')
|
||||
}}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.SETTINGS'
|
||||
)
|
||||
"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
icon="settings"
|
||||
color-scheme="secondary"
|
||||
@click="openSettings"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.DELETE')
|
||||
"
|
||||
variant="hollow"
|
||||
color-scheme="alert"
|
||||
size="small"
|
||||
icon="delete"
|
||||
@click="onClickOpenDeleteModal(portal)"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mb-12">
|
||||
<h2
|
||||
class="mb-2 text-base font-medium text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.TITLE'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<div
|
||||
class="flex justify-between mr-[6.25rem] rtl:mr-0 rtl:ml-[6.25rem] max-w-[80vw]"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.NAME'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.DOMAIN'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.custom_domain }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SLUG'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.slug }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.TITLE'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.page_title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.THEME'
|
||||
)
|
||||
}}</label>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-4 h-4 mr-1 border border-solid rounded-md rtl:mr-0 rtl:ml-1 border-slate-25 dark:border-slate-800"
|
||||
:style="{ background: portal.color }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SUB_TEXT'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.header_text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-12">
|
||||
<h2
|
||||
class="mb-2 text-base font-medium text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TITLE'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<LocaleItemTable
|
||||
:locales="locales"
|
||||
:selected-locale-code="portal.meta.default_locale"
|
||||
@changeDefaultLocale="changeDefaultLocale"
|
||||
@delete="deletePortalLocale"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="onClickDeletePortal"
|
||||
:title="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.TITLE')"
|
||||
:message="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.MESSAGE')"
|
||||
:message-value="deleteMessageValue"
|
||||
:confirm-text="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.YES')"
|
||||
:reject-text="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+28
-28
@@ -1,5 +1,31 @@
|
||||
<script>
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
export default {
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
locales: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedLocaleCode: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeDefaultLocale(localeCode) {
|
||||
this.$emit('changeDefaultLocale', { localeCode });
|
||||
},
|
||||
deleteLocale(localeCode) {
|
||||
this.$emit('delete', { localeCode });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table>
|
||||
<table class="woot-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
@@ -48,7 +74,7 @@
|
||||
)
|
||||
"
|
||||
color-scheme="warning"
|
||||
:small="true"
|
||||
small
|
||||
variant="smooth"
|
||||
class="default-status"
|
||||
/>
|
||||
@@ -95,32 +121,6 @@
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
export default {
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
locales: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedLocaleCode: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeDefaultLocale(localeCode) {
|
||||
this.$emit('change-default-locale', { localeCode });
|
||||
},
|
||||
deleteLocale(localeCode) {
|
||||
this.$emit('delete', { localeCode });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table {
|
||||
thead tr th {
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
<script>
|
||||
import PortalSwitch from './PortalSwitch.vue';
|
||||
export default {
|
||||
components: {
|
||||
PortalSwitch,
|
||||
},
|
||||
props: {
|
||||
portals: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activePortalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
closePortalPopover() {
|
||||
this.$emit('closePopover');
|
||||
},
|
||||
openPortalPage() {
|
||||
this.closePortalPopover();
|
||||
this.$router.push({
|
||||
name: 'list_all_portals',
|
||||
});
|
||||
},
|
||||
fetchPortalAndItsCategories() {
|
||||
this.$emit('fetchPortal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-on-clickaway="closePortalPopover"
|
||||
@@ -27,59 +65,21 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-600 dark:text-slate-300 mt-2">
|
||||
<p class="mt-2 text-xs text-slate-600 dark:text-slate-300">
|
||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.SUBTITLE') }}
|
||||
</p>
|
||||
</header>
|
||||
<div>
|
||||
<portal-switch
|
||||
<PortalSwitch
|
||||
v-for="portal in portals"
|
||||
:key="portal.id"
|
||||
:portal="portal"
|
||||
:active-portal-slug="activePortalSlug"
|
||||
:active-locale="activeLocale"
|
||||
:active="portal.slug === activePortalSlug"
|
||||
@open-portal-page="closePortalPopover"
|
||||
@fetch-portal="fetchPortalAndItsCategories"
|
||||
@openPortalPage="closePortalPopover"
|
||||
@fetchPortal="fetchPortalAndItsCategories"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PortalSwitch from './PortalSwitch.vue';
|
||||
export default {
|
||||
components: {
|
||||
PortalSwitch,
|
||||
},
|
||||
props: {
|
||||
portals: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activePortalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
closePortalPopover() {
|
||||
this.$emit('close-popover');
|
||||
},
|
||||
openPortalPage() {
|
||||
this.closePortalPopover();
|
||||
this.$router.push({
|
||||
name: 'list_all_portals',
|
||||
});
|
||||
},
|
||||
fetchPortalAndItsCategories() {
|
||||
this.$emit('fetch-portal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+11
-12
@@ -15,15 +15,6 @@ import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { isDomain } from 'shared/helpers/Validators';
|
||||
import SettingsLayout from './Layout/SettingsLayout.vue';
|
||||
|
||||
const { EXAMPLE_URL } = wootConstants;
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
defineComponent({
|
||||
name: 'PortalSettingsBasicForm',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
portal: {
|
||||
type: Object,
|
||||
@@ -38,6 +29,16 @@ const props = defineProps({
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['submit', 'deleteLogo']);
|
||||
|
||||
defineComponent({
|
||||
name: 'PortalSettingsBasicForm',
|
||||
});
|
||||
|
||||
const { EXAMPLE_URL } = wootConstants;
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
name: '',
|
||||
@@ -97,8 +98,6 @@ const showDeleteButton = computed(() => {
|
||||
return hasValidAvatarUrl(state.logoUrl);
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit', 'delete-logo']);
|
||||
|
||||
onMounted(() => {
|
||||
const portal = props.portal || {};
|
||||
state.name = portal.name || '';
|
||||
@@ -135,7 +134,7 @@ function onSubmitClick() {
|
||||
async function deleteAvatar() {
|
||||
state.logoUrl = '';
|
||||
state.avatarBlobId = '';
|
||||
emit('delete-logo');
|
||||
emit('deleteLogo');
|
||||
}
|
||||
|
||||
async function uploadLogoToStorage(file) {
|
||||
|
||||
+8
-9
@@ -2,20 +2,11 @@
|
||||
import { getRandomColor } from 'dashboard/helper/labelColor';
|
||||
import SettingsLayout from './Layout/SettingsLayout.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
const { EXAMPLE_URL } = wootConstants;
|
||||
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { url } from '@vuelidate/validators';
|
||||
|
||||
import { defineComponent, reactive, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
|
||||
defineComponent({
|
||||
name: 'PortalSettingsCustomizationForm',
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
portal: {
|
||||
type: Object,
|
||||
@@ -29,6 +20,14 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
defineComponent({
|
||||
name: 'PortalSettingsCustomizationForm',
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { EXAMPLE_URL } = wootConstants;
|
||||
|
||||
const state = reactive({
|
||||
color: getRandomColor(),
|
||||
pageTitle: '',
|
||||
|
||||
@@ -1,82 +1,10 @@
|
||||
<template>
|
||||
<div class="portal" :class="{ active }">
|
||||
<thumbnail :username="portal.name" variant="square" />
|
||||
<div class="actions-container">
|
||||
<header class="flex items-center justify-between mb-2.5">
|
||||
<div>
|
||||
<h3 class="text-sm mb-0.5 text-slate-700 dark:text-slate-100">
|
||||
{{ portal.name }}
|
||||
</h3>
|
||||
<p class="text-slate-600 dark:text-slate-200 mb-0 text-xs">
|
||||
{{ articlesCount }}
|
||||
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }}
|
||||
</p>
|
||||
</div>
|
||||
<woot-label
|
||||
v-if="active"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="success"
|
||||
:title="$t('HELP_CENTER.PORTAL.ACTIVE_BADGE')"
|
||||
/>
|
||||
</header>
|
||||
<div class="portal-locales">
|
||||
<h5 class="text-base text-slate-700 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }}
|
||||
</h5>
|
||||
<ul>
|
||||
<li v-for="locale in locales" :key="locale.code">
|
||||
<woot-button
|
||||
:variant="`locale-item ${
|
||||
isLocaleActive(locale.code, activePortalSlug)
|
||||
? 'smooth'
|
||||
: 'clear'
|
||||
}`"
|
||||
size="large"
|
||||
color-scheme="secondary"
|
||||
@click="event => onClick(event, locale.code, portal)"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="meta">
|
||||
<h6 class="text-sm text-left mb-0.5">
|
||||
<span class="text-slate-700 dark:text-slate-100">
|
||||
{{ localeName(locale.code) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isLocaleDefault(locale.code)"
|
||||
class="text-sm text-slate-300 dark:text-slate-200"
|
||||
>
|
||||
{{ `(${$t('HELP_CENTER.PORTAL.DEFAULT')})` }}
|
||||
</span>
|
||||
</h6>
|
||||
|
||||
<span
|
||||
class="flex text-slate-600 dark:text-slate-200 text-sm text-left leading-4 w-full"
|
||||
>
|
||||
{{ locale.articles_count }}
|
||||
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }} -
|
||||
{{ locale.code }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isLocaleActive(locale.code, activePortalSlug)">
|
||||
<fluent-icon icon="checkmark" class="locale__radio" />
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
thumbnail,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
@@ -126,8 +54,8 @@ export default {
|
||||
locale: code,
|
||||
},
|
||||
});
|
||||
this.$emit('fetch-portal');
|
||||
this.$emit('open-portal-page');
|
||||
this.$emit('fetchPortal');
|
||||
this.$emit('openPortalPage');
|
||||
},
|
||||
isLocaleActive(code, slug) {
|
||||
const isPortalActive = this.portal.slug === slug;
|
||||
@@ -141,6 +69,78 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="portal" :class="{ active }">
|
||||
<Thumbnail :username="portal.name" variant="square" />
|
||||
<div class="actions-container">
|
||||
<header class="flex items-center justify-between mb-2.5">
|
||||
<div>
|
||||
<h3 class="text-sm mb-0.5 text-slate-700 dark:text-slate-100">
|
||||
{{ portal.name }}
|
||||
</h3>
|
||||
<p class="mb-0 text-xs text-slate-600 dark:text-slate-200">
|
||||
{{ articlesCount }}
|
||||
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }}
|
||||
</p>
|
||||
</div>
|
||||
<woot-label
|
||||
v-if="active"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="success"
|
||||
:title="$t('HELP_CENTER.PORTAL.ACTIVE_BADGE')"
|
||||
/>
|
||||
</header>
|
||||
<div class="portal-locales">
|
||||
<h5 class="text-base text-slate-700 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }}
|
||||
</h5>
|
||||
<ul>
|
||||
<li v-for="locale in locales" :key="locale.code">
|
||||
<woot-button
|
||||
:variant="`locale-item ${
|
||||
isLocaleActive(locale.code, activePortalSlug)
|
||||
? 'smooth'
|
||||
: 'clear'
|
||||
}`"
|
||||
size="large"
|
||||
color-scheme="secondary"
|
||||
@click="event => onClick(event, locale.code, portal)"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="meta">
|
||||
<h6 class="text-sm text-left mb-0.5">
|
||||
<span class="text-slate-700 dark:text-slate-100">
|
||||
{{ localeName(locale.code) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isLocaleDefault(locale.code)"
|
||||
class="text-sm text-slate-300 dark:text-slate-200"
|
||||
>
|
||||
{{ `(${$t('HELP_CENTER.PORTAL.DEFAULT')})` }}
|
||||
</span>
|
||||
</h6>
|
||||
|
||||
<span
|
||||
class="flex w-full text-sm leading-4 text-left text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ locale.articles_count }}
|
||||
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }} -
|
||||
{{ locale.code }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isLocaleActive(locale.code, activePortalSlug)">
|
||||
<fluent-icon icon="checkmark" class="locale__radio" />
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.portal {
|
||||
@apply bg-white dark:bg-slate-800 rounded-md p-4 relative flex mb-4 border border-solid border-slate-100 dark:border-slate-600;
|
||||
|
||||
+41
-41
@@ -1,42 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-full overflow-auto w-60 flex flex-col bg-white dark:bg-slate-900 border-r dark:border-slate-700 rtl:border-r-0 rtl:border-l border-slate-50 text-sm"
|
||||
>
|
||||
<sidebar-header
|
||||
:thumbnail-src="thumbnailSrc"
|
||||
:header-title="headerTitle"
|
||||
:sub-title="subTitle"
|
||||
:portal-link="portalLink"
|
||||
class="px-4"
|
||||
@open-popover="openPortalPopover"
|
||||
/>
|
||||
<transition-group
|
||||
name="menu-list"
|
||||
tag="ul"
|
||||
class="py-2 px-4 list-none ml-0 mb-0"
|
||||
>
|
||||
<secondary-nav-item
|
||||
v-for="menuItem in accessibleMenuItems"
|
||||
:key="menuItem.toState"
|
||||
:menu-item="menuItem"
|
||||
/>
|
||||
<secondary-nav-item
|
||||
v-for="menuItem in additionalSecondaryMenuItems"
|
||||
:key="menuItem.key"
|
||||
:menu-item="menuItem"
|
||||
@open="onClickOpenAddCatogoryModal"
|
||||
/>
|
||||
<p
|
||||
v-if="!hasCategory"
|
||||
key="empty-category-nessage"
|
||||
class="p-1.5 px-4 text-slate-300"
|
||||
>
|
||||
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
|
||||
</p>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SecondaryNavItem from 'dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue';
|
||||
import SidebarHeader from './SidebarHeader.vue';
|
||||
@@ -92,11 +53,50 @@ export default {
|
||||
this.$emit('input', value);
|
||||
},
|
||||
openPortalPopover() {
|
||||
this.$emit('open-popover');
|
||||
this.$emit('openPopover');
|
||||
},
|
||||
onClickOpenAddCatogoryModal() {
|
||||
this.$emit('open-modal');
|
||||
this.$emit('openModal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col h-full overflow-auto text-sm bg-white border-r w-60 dark:bg-slate-900 dark:border-slate-700 rtl:border-r-0 rtl:border-l border-slate-50"
|
||||
>
|
||||
<SidebarHeader
|
||||
:thumbnail-src="thumbnailSrc"
|
||||
:header-title="headerTitle"
|
||||
:sub-title="subTitle"
|
||||
:portal-link="portalLink"
|
||||
class="px-4"
|
||||
@openPopover="openPortalPopover"
|
||||
/>
|
||||
<transition-group
|
||||
name="menu-list"
|
||||
tag="ul"
|
||||
class="px-4 py-2 mb-0 ml-0 list-none"
|
||||
>
|
||||
<SecondaryNavItem
|
||||
v-for="menuItem in accessibleMenuItems"
|
||||
:key="menuItem.toState"
|
||||
:menu-item="menuItem"
|
||||
/>
|
||||
<SecondaryNavItem
|
||||
v-for="menuItem in additionalSecondaryMenuItems"
|
||||
:key="menuItem.key"
|
||||
:menu-item="menuItem"
|
||||
@open="onClickOpenAddCatogoryModal"
|
||||
/>
|
||||
<p
|
||||
v-if="!hasCategory"
|
||||
key="empty-category-nessage"
|
||||
class="p-1.5 px-4 text-slate-300"
|
||||
>
|
||||
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
|
||||
</p>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+42
-42
@@ -1,44 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex h-16 items-center justify-between py-4 px-0 mb-1/4 border-b border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<thumbnail
|
||||
size="32px"
|
||||
:src="thumbnailSrc"
|
||||
:username="headerTitle"
|
||||
variant="square"
|
||||
/>
|
||||
<div class="flex items-start flex-col ml-2 rtl:ml-0 rtl:mr-2">
|
||||
<h4
|
||||
class="text-sm w-28 h-4 mb-0 leading-4 overflow-hidden whitespace-nowrap text-ellipsis text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h4>
|
||||
<span class="h-4 leading-4 text-slate-600 dark:text-slate-200 text-xs">
|
||||
{{ subTitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="arrow-up-right"
|
||||
@click="popoutHelpCenter"
|
||||
/>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="arrow-swap"
|
||||
@click="openPortalPopover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
export default {
|
||||
@@ -68,8 +27,49 @@ export default {
|
||||
window.open(this.portalLink, '_blank');
|
||||
},
|
||||
openPortalPopover() {
|
||||
this.$emit('open-popover');
|
||||
this.$emit('openPopover');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between h-16 px-0 py-4 border-b mb-1/4 border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Thumbnail
|
||||
size="32px"
|
||||
:src="thumbnailSrc"
|
||||
:username="headerTitle"
|
||||
variant="square"
|
||||
/>
|
||||
<div class="flex flex-col items-start ml-2 rtl:ml-0 rtl:mr-2">
|
||||
<h4
|
||||
class="h-4 mb-0 overflow-hidden text-sm leading-4 w-28 whitespace-nowrap text-ellipsis text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h4>
|
||||
<span class="h-4 text-xs leading-4 text-slate-600 dark:text-slate-200">
|
||||
{{ subTitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="arrow-up-right"
|
||||
@click="popoutHelpCenter"
|
||||
/>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="arrow-swap"
|
||||
@click="openPortalPopover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+14
-14
@@ -1,17 +1,3 @@
|
||||
<template>
|
||||
<div class="search-input--wrap">
|
||||
<div class="search-icon--wrap">
|
||||
<fluent-icon icon="search" size="18" class="search-icon" />
|
||||
</div>
|
||||
<input
|
||||
v-model="searchValue"
|
||||
class="search-input"
|
||||
:placeholder="$t('HELP_CENTER.SIDEBAR.SEARCH.PLACEHOLDER')"
|
||||
@input="onSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
@@ -27,6 +13,20 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-input--wrap">
|
||||
<div class="search-icon--wrap">
|
||||
<fluent-icon icon="search" size="18" class="search-icon" />
|
||||
</div>
|
||||
<input
|
||||
v-model="searchValue"
|
||||
class="search-input"
|
||||
:placeholder="$t('HELP_CENTER.SIDEBAR.SEARCH.PLACEHOLDER')"
|
||||
@input="onSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-input--wrap {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,66 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
helpCenterDocsURL: wootConstants.HELP_CENTER_DOCS_URL,
|
||||
upgradeFeature: [
|
||||
{
|
||||
key: 1,
|
||||
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.PORTALS.TITLE'),
|
||||
icon: 'book-copy',
|
||||
description: this.$t(
|
||||
'HELP_CENTER.UPGRADE_PAGE.FEATURES.PORTALS.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.LOCALES.TITLE'),
|
||||
icon: 'globe-line',
|
||||
description: this.$t(
|
||||
'HELP_CENTER.UPGRADE_PAGE.FEATURES.LOCALES.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.SEO.TITLE'),
|
||||
icon: 'heart-handshake',
|
||||
description: this.$t(
|
||||
'HELP_CENTER.UPGRADE_PAGE.FEATURES.SEO.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.API.TITLE'),
|
||||
icon: 'search-check',
|
||||
description: this.$t(
|
||||
'HELP_CENTER.UPGRADE_PAGE.FEATURES.API.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', // Pending change text
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
openBillingPage() {
|
||||
this.$router.push({
|
||||
name: 'billing_settings_index',
|
||||
params: { accountId: this.accountId },
|
||||
});
|
||||
},
|
||||
openHelpCenterDocs() {
|
||||
window.open(this.helpCenterDocsURL, '_blank');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-12 sm:gap-16 items-center justify-center py-0 px-4 md:px-0 w-full h-full max-w-full overflow-auto bg-white dark:bg-slate-900"
|
||||
@@ -70,66 +133,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
helpCenterDocsURL: wootConstants.HELP_CENTER_DOCS_URL,
|
||||
upgradeFeature: [
|
||||
{
|
||||
key: 1,
|
||||
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.PORTALS.TITLE'),
|
||||
icon: 'book-copy',
|
||||
description: this.$t(
|
||||
'HELP_CENTER.UPGRADE_PAGE.FEATURES.PORTALS.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.LOCALES.TITLE'),
|
||||
icon: 'globe-line',
|
||||
description: this.$t(
|
||||
'HELP_CENTER.UPGRADE_PAGE.FEATURES.LOCALES.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.SEO.TITLE'),
|
||||
icon: 'heart-handshake',
|
||||
description: this.$t(
|
||||
'HELP_CENTER.UPGRADE_PAGE.FEATURES.SEO.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.API.TITLE'),
|
||||
icon: 'search-check',
|
||||
description: this.$t(
|
||||
'HELP_CENTER.UPGRADE_PAGE.FEATURES.API.DESCRIPTION'
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', // Pending change text
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
openBillingPage() {
|
||||
this.$router.push({
|
||||
name: 'billing_settings_index',
|
||||
params: { accountId: this.accountId },
|
||||
});
|
||||
},
|
||||
openHelpCenterDocs() {
|
||||
window.open(this.helpCenterDocsURL, '_blank');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,42 +1,48 @@
|
||||
import HelpCenterLayout from './components/HelpCenterLayout';
|
||||
import HelpCenterLayout from './components/HelpCenterLayout.vue';
|
||||
import { getPortalRoute } from './helpers/routeHelper';
|
||||
|
||||
const ListAllPortals = () => import('./pages/portals/ListAllPortals');
|
||||
const NewPortal = () => import('./pages/portals/NewPortal');
|
||||
const ListAllPortals = () => import('./pages/portals/ListAllPortals.vue');
|
||||
const NewPortal = () => import('./pages/portals/NewPortal.vue');
|
||||
|
||||
const EditPortal = () => import('./pages/portals/EditPortal');
|
||||
const EditPortalBasic = () => import('./pages/portals/EditPortalBasic');
|
||||
const EditPortal = () => import('./pages/portals/EditPortal.vue');
|
||||
const EditPortalBasic = () => import('./pages/portals/EditPortalBasic.vue');
|
||||
const EditPortalCustomization = () =>
|
||||
import('./pages/portals/EditPortalCustomization');
|
||||
import('./pages/portals/EditPortalCustomization.vue');
|
||||
const EditPortalLocales = () => import('./pages/portals/EditPortalLocales.vue');
|
||||
const ShowPortal = () => import('./pages/portals/ShowPortal');
|
||||
const PortalDetails = () => import('./pages/portals/PortalDetails');
|
||||
const PortalCustomization = () => import('./pages/portals/PortalCustomization');
|
||||
const ShowPortal = () => import('./pages/portals/ShowPortal.vue');
|
||||
const PortalDetails = () => import('./pages/portals/PortalDetails.vue');
|
||||
const PortalCustomization = () =>
|
||||
import('./pages/portals/PortalCustomization.vue');
|
||||
const PortalSettingsFinish = () =>
|
||||
import('./pages/portals/PortalSettingsFinish');
|
||||
import('./pages/portals/PortalSettingsFinish.vue');
|
||||
|
||||
const ListAllCategories = () => import('./pages/categories/ListAllCategories');
|
||||
const NewCategory = () => import('./pages/categories/NewCategory');
|
||||
const EditCategory = () => import('./pages/categories/EditCategory');
|
||||
const ListAllCategories = () =>
|
||||
import('./pages/categories/ListAllCategories.vue');
|
||||
const NewCategory = () => import('./pages/categories/NewCategory.vue');
|
||||
const EditCategory = () => import('./pages/categories/EditCategory.vue');
|
||||
const ListCategoryArticles = () =>
|
||||
import('./pages/articles/ListCategoryArticles');
|
||||
const ListAllArticles = () => import('./pages/articles/ListAllArticles');
|
||||
import('./pages/articles/ListCategoryArticles.vue');
|
||||
const ListAllArticles = () => import('./pages/articles/ListAllArticles.vue');
|
||||
const DefaultPortalArticles = () =>
|
||||
import('./pages/articles/DefaultPortalArticles');
|
||||
const NewArticle = () => import('./pages/articles/NewArticle');
|
||||
const EditArticle = () => import('./pages/articles/EditArticle');
|
||||
import('./pages/articles/DefaultPortalArticles.vue');
|
||||
const NewArticle = () => import('./pages/articles/NewArticle.vue');
|
||||
const EditArticle = () => import('./pages/articles/EditArticle.vue');
|
||||
|
||||
const portalRoutes = [
|
||||
{
|
||||
path: getPortalRoute(''),
|
||||
name: 'default_portal_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: DefaultPortalArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute('all'),
|
||||
name: 'list_all_portals',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllPortals,
|
||||
},
|
||||
{
|
||||
@@ -47,55 +53,73 @@ const portalRoutes = [
|
||||
path: '',
|
||||
name: 'new_portal_information',
|
||||
component: PortalDetails,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':portalSlug/customization',
|
||||
name: 'portal_customization',
|
||||
component: PortalCustomization,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':portalSlug/finish',
|
||||
name: 'portal_finish',
|
||||
component: PortalSettingsFinish,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug'),
|
||||
name: 'portalSlug',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ShowPortal,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/edit'),
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditPortal,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'edit_portal_information',
|
||||
component: EditPortalBasic,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'customizations',
|
||||
name: 'edit_portal_customization',
|
||||
component: EditPortalCustomization,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'locales',
|
||||
name: 'edit_portal_locales',
|
||||
component: EditPortalLocales,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'list_all_locale_categories',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
],
|
||||
@@ -106,39 +130,51 @@ const articleRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles'),
|
||||
name: 'list_all_locale_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/new'),
|
||||
name: 'new_article',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: NewArticle,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/mine'),
|
||||
name: 'list_mine_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/archived'),
|
||||
name: 'list_archived_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/draft'),
|
||||
name: 'list_draft_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
|
||||
name: 'edit_article',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditArticle,
|
||||
},
|
||||
];
|
||||
@@ -147,19 +183,25 @@ const categoryRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories'),
|
||||
name: 'all_locale_categories',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/new'),
|
||||
name: 'new_category_in_locale',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: NewCategory,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'show_category',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
@@ -167,13 +209,17 @@ const categoryRoutes = [
|
||||
':portalSlug/:locale/categories/:categorySlug/articles'
|
||||
),
|
||||
name: 'show_category_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListCategoryArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'edit_category',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditCategory,
|
||||
},
|
||||
];
|
||||
|
||||
+129
-130
@@ -1,127 +1,3 @@
|
||||
<template>
|
||||
<transition name="popover-animation">
|
||||
<div
|
||||
class="min-w-[15rem] max-w-[22.5rem] p-6 overflow-y-auto border-l rtl:border-r rtl:border-l-0 border-solid border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<h3 class="text-base text-slate-800 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.TITLE') }}
|
||||
</h3>
|
||||
<div class="mt-4 mb-6">
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.LABEL') }}
|
||||
<multiselect-dropdown
|
||||
:options="categories"
|
||||
:selected-item="selectedCategory"
|
||||
:has-thumbnail="false"
|
||||
:multiselector-title="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.TITLE')
|
||||
"
|
||||
:multiselector-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.NO_RESULT')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t(
|
||||
'HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.SEARCH_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
@click="onClickSelectCategory"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.LABEL') }}
|
||||
<multiselect-dropdown
|
||||
:options="agents"
|
||||
:selected-item="assignedAuthor"
|
||||
:multiselector-title="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.TITLE')
|
||||
"
|
||||
:multiselector-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.NO_RESULT')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
@click="onClickAssignAuthor"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.LABEL') }}
|
||||
<textarea
|
||||
v-model="metaTitle"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.PLACEHOLDER')
|
||||
"
|
||||
@input="onChangeMetaInput"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="metaDescription"
|
||||
class="text-sm"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t(
|
||||
'HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
@input="onChangeMetaInput"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.LABEL') }}
|
||||
<multiselect
|
||||
ref="tagInput"
|
||||
v-model="metaTags"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.PLACEHOLDER')
|
||||
"
|
||||
label="name"
|
||||
:options="metaOptions"
|
||||
track-by="name"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:close-on-select="false"
|
||||
@search-change="handleSearchChange"
|
||||
@close="onBlur"
|
||||
@tag="addTagValue"
|
||||
@remove="removeTag"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<woot-button
|
||||
icon="archive"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="onClickArchiveArticle"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.BUTTONS.ARCHIVE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
icon="delete"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="alert"
|
||||
@click="onClickDeleteArticle"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.BUTTONS.DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -180,7 +56,7 @@ export default {
|
||||
mounted() {
|
||||
this.saveArticle = debounce(
|
||||
() => {
|
||||
this.$emit('save-article', {
|
||||
this.$emit('saveArticle', {
|
||||
meta: {
|
||||
title: this.metaTitle,
|
||||
description: this.metaDescription,
|
||||
@@ -219,30 +95,153 @@ export default {
|
||||
}
|
||||
},
|
||||
onClickSelectCategory({ id }) {
|
||||
this.$emit('save-article', { category_id: id });
|
||||
this.$emit('saveArticle', { category_id: id });
|
||||
},
|
||||
onClickAssignAuthor({ id }) {
|
||||
this.$emit('save-article', { author_id: id });
|
||||
this.$emit('saveArticle', { author_id: id });
|
||||
this.updateMeta();
|
||||
},
|
||||
onChangeMetaInput() {
|
||||
this.saveArticle();
|
||||
},
|
||||
onClickArchiveArticle() {
|
||||
this.$emit('archive-article');
|
||||
this.$emit('archiveArticle');
|
||||
this.updateMeta();
|
||||
},
|
||||
onClickDeleteArticle() {
|
||||
this.$emit('delete-article');
|
||||
this.$emit('deleteArticle');
|
||||
this.updateMeta();
|
||||
},
|
||||
updateMeta() {
|
||||
this.$emit('update-meta');
|
||||
this.$emit('updateMeta');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="popover-animation">
|
||||
<div
|
||||
class="min-w-[15rem] max-w-[22.5rem] p-6 overflow-y-auto border-l rtl:border-r rtl:border-l-0 border-solid border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<h3 class="text-base text-slate-800 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.TITLE') }}
|
||||
</h3>
|
||||
<div class="mt-4 mb-6">
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.LABEL') }}
|
||||
<MultiselectDropdown
|
||||
:options="categories"
|
||||
:selected-item="selectedCategory"
|
||||
:has-thumbnail="false"
|
||||
:multiselector-title="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.TITLE')
|
||||
"
|
||||
:multiselector-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.NO_RESULT')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t(
|
||||
'HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.SEARCH_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
@click="onClickSelectCategory"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.LABEL') }}
|
||||
<MultiselectDropdown
|
||||
:options="agents"
|
||||
:selected-item="assignedAuthor"
|
||||
:multiselector-title="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.TITLE')
|
||||
"
|
||||
:multiselector-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.NO_RESULT')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
@click="onClickAssignAuthor"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.LABEL') }}
|
||||
<textarea
|
||||
v-model="metaTitle"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.PLACEHOLDER')
|
||||
"
|
||||
@input="onChangeMetaInput"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="metaDescription"
|
||||
class="text-sm"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t(
|
||||
'HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
@input="onChangeMetaInput"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.LABEL') }}
|
||||
<multiselect
|
||||
v-model="metaTags"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.PLACEHOLDER')
|
||||
"
|
||||
label="name"
|
||||
:options="metaOptions"
|
||||
track-by="name"
|
||||
multiple
|
||||
taggable
|
||||
:close-on-select="false"
|
||||
@search-change="handleSearchChange"
|
||||
@close="onBlur"
|
||||
@tag="addTagValue"
|
||||
@remove="removeTag"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<woot-button
|
||||
icon="archive"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="onClickArchiveArticle"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.BUTTONS.ARCHIVE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
icon="delete"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="alert"
|
||||
@click="onClickDeleteArticle"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.BUTTONS.DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.multiselect {
|
||||
|
||||
+16
-10
@@ -1,17 +1,15 @@
|
||||
<template>
|
||||
<div
|
||||
class="text-slate-600 dark:text-slate-200 flex items-center justify-center w-full"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
mixins: [uiSettingsMixin],
|
||||
setup() {
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ portals: 'portals/allPortals' }),
|
||||
},
|
||||
@@ -60,3 +58,11 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center w-full text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('HELP_CENTER.LOADING') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+53
-54
@@ -1,59 +1,11 @@
|
||||
<template>
|
||||
<div class="article-container flex w-full overflow-auto">
|
||||
<div
|
||||
class="flex-1 flex-shrink-0 overflow-auto px-6"
|
||||
:class="{ 'flex-grow-1 flex-shrink-0': showArticleSettings }"
|
||||
>
|
||||
<edit-article-header
|
||||
:back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
|
||||
:is-updating="isUpdating"
|
||||
:is-saved="isSaved"
|
||||
:is-sidebar-open="showArticleSettings"
|
||||
@back="onClickGoBack"
|
||||
@open="openArticleSettings"
|
||||
@close="closeArticleSettings"
|
||||
@show="showArticleInPortal"
|
||||
@update-meta="updateMeta"
|
||||
/>
|
||||
<div v-if="isFetching" class="text-center p-4 text-base h-full">
|
||||
<spinner size="" />
|
||||
<span>{{ $t('HELP_CENTER.EDIT_ARTICLE.LOADING') }}</span>
|
||||
</div>
|
||||
<article-editor
|
||||
v-else
|
||||
:is-settings-sidebar-open="showArticleSettings"
|
||||
:article="article"
|
||||
@save-article="saveArticle"
|
||||
/>
|
||||
</div>
|
||||
<article-settings
|
||||
v-if="showArticleSettings"
|
||||
:article="article"
|
||||
@save-article="saveArticle"
|
||||
@delete-article="openDeletePopup"
|
||||
@archive-article="archiveArticle"
|
||||
@update-meta="updateMeta"
|
||||
/>
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.TITLE')"
|
||||
:message="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.MESSAGE')"
|
||||
:confirm-text="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.YES')"
|
||||
:reject-text="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import EditArticleHeader from '../../components/Header/EditArticleHeader.vue';
|
||||
import ArticleEditor from '../../components/ArticleEditor.vue';
|
||||
import ArticleSettings from './ArticleSettings.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import portalMixin from '../../mixins/portalMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
@@ -66,7 +18,7 @@ export default {
|
||||
Spinner,
|
||||
ArticleSettings,
|
||||
},
|
||||
mixins: [portalMixin, alertMixin],
|
||||
mixins: [portalMixin],
|
||||
data() {
|
||||
return {
|
||||
isUpdating: false,
|
||||
@@ -79,7 +31,6 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFetching: 'articles/isFetching',
|
||||
articles: 'articles/articles',
|
||||
}),
|
||||
article() {
|
||||
return this.$store.getters['articles/articleById'](this.articleId);
|
||||
@@ -144,7 +95,7 @@ export default {
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message || this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR');
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this.isUpdating = false;
|
||||
@@ -174,7 +125,7 @@ export default {
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.DELETE_ARTICLE.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
async archiveArticle() {
|
||||
@@ -190,7 +141,7 @@ export default {
|
||||
this.alertMessage =
|
||||
error?.message || this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.ERROR');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
updateMeta() {
|
||||
@@ -215,3 +166,51 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full overflow-auto article-container">
|
||||
<div
|
||||
class="flex-1 flex-shrink-0 px-6 overflow-auto"
|
||||
:class="{ 'flex-grow-1 flex-shrink-0': showArticleSettings }"
|
||||
>
|
||||
<EditArticleHeader
|
||||
:back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
|
||||
:is-updating="isUpdating"
|
||||
:is-saved="isSaved"
|
||||
:is-sidebar-open="showArticleSettings"
|
||||
@back="onClickGoBack"
|
||||
@open="openArticleSettings"
|
||||
@close="closeArticleSettings"
|
||||
@show="showArticleInPortal"
|
||||
@updateMeta="updateMeta"
|
||||
/>
|
||||
<div v-if="isFetching" class="h-full p-4 text-base text-center">
|
||||
<Spinner size="" />
|
||||
<span>{{ $t('HELP_CENTER.EDIT_ARTICLE.LOADING') }}</span>
|
||||
</div>
|
||||
<ArticleEditor
|
||||
v-else
|
||||
:is-settings-sidebar-open="showArticleSettings"
|
||||
:article="article"
|
||||
@saveArticle="saveArticle"
|
||||
/>
|
||||
</div>
|
||||
<ArticleSettings
|
||||
v-if="showArticleSettings"
|
||||
:article="article"
|
||||
@saveArticle="saveArticle"
|
||||
@deleteArticle="openDeletePopup"
|
||||
@archiveArticle="archiveArticle"
|
||||
@updateMeta="updateMeta"
|
||||
/>
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.TITLE')"
|
||||
:message="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.MESSAGE')"
|
||||
:confirm-text="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.YES')"
|
||||
:reject-text="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+40
-41
@@ -1,42 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="py-0 px-0 w-full max-w-full overflow-auto bg-white dark:bg-slate-900 flex flex-col"
|
||||
>
|
||||
<article-header
|
||||
:header-title="headerTitle"
|
||||
:count="meta.count"
|
||||
:selected-locale="activeLocaleName"
|
||||
:all-locales="allowedLocales"
|
||||
selected-value="Published"
|
||||
class="border-b border-slate-50 dark:border-slate-700"
|
||||
@new-article-page="newArticlePage"
|
||||
@change-locale="onChangeLocale"
|
||||
/>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="items-center flex text-base justify-center py-6 px-4 text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
<spinner />
|
||||
<span class="text-slate-600 dark:text-slate-200">
|
||||
{{ $t('HELP_CENTER.TABLE.LOADING_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<empty-state
|
||||
v-else-if="shouldShowEmptyState"
|
||||
:title="$t('HELP_CENTER.TABLE.NO_ARTICLES')"
|
||||
/>
|
||||
<div v-else class="flex flex-1">
|
||||
<article-table
|
||||
:articles="articles"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="Number(meta.count)"
|
||||
@page-change="onPageChange"
|
||||
@reorder="onReorder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
@@ -62,7 +23,6 @@ export default {
|
||||
...mapGetters({
|
||||
articles: 'articles/allArticles',
|
||||
categories: 'categories/allCategories',
|
||||
uiFlags: 'articles/uiFlags',
|
||||
meta: 'articles/getMeta',
|
||||
isFetching: 'articles/isFetching',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
@@ -188,8 +148,47 @@ export default {
|
||||
locale,
|
||||
},
|
||||
});
|
||||
this.$emit('reload-locale');
|
||||
this.$emit('reloadLocale');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col w-full max-w-full px-0 py-0 overflow-auto bg-white dark:bg-slate-900"
|
||||
>
|
||||
<ArticleHeader
|
||||
:header-title="headerTitle"
|
||||
:count="meta.count"
|
||||
:selected-locale="activeLocaleName"
|
||||
:all-locales="allowedLocales"
|
||||
selected-value="Published"
|
||||
class="border-b border-slate-50 dark:border-slate-700"
|
||||
@newArticlePage="newArticlePage"
|
||||
@changeLocale="onChangeLocale"
|
||||
/>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center px-4 py-6 text-base text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
<Spinner />
|
||||
<span class="text-slate-600 dark:text-slate-200">
|
||||
{{ $t('HELP_CENTER.TABLE.LOADING_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else-if="shouldShowEmptyState"
|
||||
:title="$t('HELP_CENTER.TABLE.NO_ARTICLES')"
|
||||
/>
|
||||
<div v-else class="flex flex-1">
|
||||
<ArticleTable
|
||||
:articles="articles"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="Number(meta.count)"
|
||||
@pageChange="onPageChange"
|
||||
@reorder="onReorder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<template>
|
||||
<div>Component to list articles in a category in a portal</div>
|
||||
<div>{{ 'Component to list articles in a category in a portal' }}</div>
|
||||
</template>
|
||||
|
||||
+29
-30
@@ -1,34 +1,9 @@
|
||||
<template>
|
||||
<div class="flex flex-1 overflow-auto">
|
||||
<div
|
||||
class="flex-1 overflow-y-auto flex-shrink-0 px-6"
|
||||
:class="{ 'flex-grow-1': showArticleSettings }"
|
||||
>
|
||||
<edit-article-header
|
||||
:back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
|
||||
draft-state="saved"
|
||||
:is-sidebar-open="showArticleSettings"
|
||||
@back="onClickGoBack"
|
||||
@open="openArticleSettings"
|
||||
@close="closeArticleSettings"
|
||||
@save-article="createNewArticle"
|
||||
/>
|
||||
<article-editor :article="newArticle" @save-article="createNewArticle" />
|
||||
</div>
|
||||
<article-settings
|
||||
v-if="showArticleSettings"
|
||||
:article="article"
|
||||
@save-article="saveArticle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader.vue';
|
||||
import ArticleEditor from '../../components/ArticleEditor.vue';
|
||||
import portalMixin from '../../mixins/portalMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin.js';
|
||||
import ArticleSettings from './ArticleSettings.vue';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
export default {
|
||||
@@ -37,7 +12,7 @@ export default {
|
||||
ArticleEditor,
|
||||
ArticleSettings,
|
||||
},
|
||||
mixins: [portalMixin, alertMixin],
|
||||
mixins: [portalMixin],
|
||||
data() {
|
||||
return {
|
||||
articleTitle: '',
|
||||
@@ -50,7 +25,6 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUserID: 'getCurrentUserID',
|
||||
articles: 'articles/articles',
|
||||
categories: 'categories/allCategories',
|
||||
}),
|
||||
articleId() {
|
||||
@@ -100,7 +74,7 @@ export default {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.CREATE_ARTICLE.API.ERROR_MESSAGE');
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -112,8 +86,33 @@ export default {
|
||||
},
|
||||
saveArticle() {
|
||||
this.alertMessage = this.$t('HELP_CENTER.CREATE_ARTICLE.ERROR_MESSAGE');
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-1 overflow-auto">
|
||||
<div
|
||||
class="flex-1 flex-shrink-0 px-6 overflow-y-auto"
|
||||
:class="{ 'flex-grow-1': showArticleSettings }"
|
||||
>
|
||||
<EditArticleHeader
|
||||
:back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
|
||||
draft-state="saved"
|
||||
:is-sidebar-open="showArticleSettings"
|
||||
@back="onClickGoBack"
|
||||
@open="openArticleSettings"
|
||||
@close="closeArticleSettings"
|
||||
@save-article="createNewArticle"
|
||||
/>
|
||||
<ArticleEditor :article="newArticle" @saveArticle="createNewArticle" />
|
||||
</div>
|
||||
<ArticleSettings
|
||||
v-if="showArticleSettings"
|
||||
:article="article"
|
||||
@saveArticle="saveArticle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+79
-75
@@ -1,81 +1,13 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('HELP_CENTER.CATEGORY.ADD.TITLE')"
|
||||
:header-content="$t('HELP_CENTER.CATEGORY.ADD.SUB_TITLE')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="onCreate">
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row w-full mt-0 mx-0 mb-4">
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.PORTAL') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ portalName }}</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.LOCALE') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ locale }}</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<category-name-icon-input
|
||||
:label="$t('HELP_CENTER.CATEGORY.ADD.NAME.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.ADD.NAME.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.ADD.NAME.HELP_TEXT')"
|
||||
:has-error="$v.name.$error"
|
||||
:error-message="$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR')"
|
||||
@name-change="onNameChange"
|
||||
@icon-change="onClickInsertEmoji"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="slug"
|
||||
:class="{ error: $v.slug.$error }"
|
||||
class="w-full"
|
||||
:error="slugError"
|
||||
:label="$t('HELP_CENTER.CATEGORY.ADD.SLUG.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.ADD.SLUG.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.ADD.SLUG.HELP_TEXT')"
|
||||
@input="$v.slug.$touch"
|
||||
/>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button @click="addCategory">
|
||||
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
import CategoryNameIconInput from './NameEmojiInput.vue';
|
||||
|
||||
export default {
|
||||
components: { CategoryNameIconInput },
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -94,6 +26,9 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
@@ -118,7 +53,7 @@ export default {
|
||||
: this.portalSlug;
|
||||
},
|
||||
slugError() {
|
||||
if (this.$v.slug.$error) {
|
||||
if (this.v$.slug.$error) {
|
||||
return this.$t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
|
||||
}
|
||||
return '';
|
||||
@@ -148,8 +83,8 @@ export default {
|
||||
description,
|
||||
locale,
|
||||
};
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -169,12 +104,81 @@ export default {
|
||||
this.alertMessage =
|
||||
errorMessage || this.$t('HELP_CENTER.CATEGORY.ADD.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('HELP_CENTER.CATEGORY.ADD.TITLE')"
|
||||
:header-content="$t('HELP_CENTER.CATEGORY.ADD.SUB_TITLE')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="onCreate">
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row w-full mx-0 mt-0 mb-4">
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.PORTAL') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ portalName }}</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.LOCALE') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ locale }}</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<CategoryNameIconInput
|
||||
:label="$t('HELP_CENTER.CATEGORY.ADD.NAME.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.ADD.NAME.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.ADD.NAME.HELP_TEXT')"
|
||||
:has-error="v$.name.$error"
|
||||
:error-message="$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR')"
|
||||
@nameChange="onNameChange"
|
||||
@iconChange="onClickInsertEmoji"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="slug"
|
||||
:class="{ error: v$.slug.$error }"
|
||||
class="w-full"
|
||||
:error="slugError"
|
||||
:label="$t('HELP_CENTER.CATEGORY.ADD.SLUG.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.ADD.SLUG.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.ADD.SLUG.HELP_TEXT')"
|
||||
@input="v$.slug.$touch"
|
||||
/>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button @click="addCategory">
|
||||
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.input-container::v-deep {
|
||||
@apply mt-0 mb-4 mx-0;
|
||||
|
||||
+22
-22
@@ -1,6 +1,26 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
categories: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
editCategory(category) {
|
||||
this.$emit('edit', category);
|
||||
},
|
||||
deleteCategory(category) {
|
||||
this.$emit('delete', category);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<table>
|
||||
<table class="woot-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
@@ -66,33 +86,13 @@
|
||||
</table>
|
||||
<p
|
||||
v-if="categories.length === 0"
|
||||
class="flex justify-center text-slate-500 dark:text-slate-300 text-base mt-8"
|
||||
class="flex justify-center mt-8 text-base text-slate-500 dark:text-slate-300"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TABLE.EMPTY_TEXT') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
categories: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
editCategory(category) {
|
||||
this.$emit('edit', category);
|
||||
},
|
||||
deleteCategory(category) {
|
||||
this.$emit('delete', category);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table {
|
||||
thead tr th {
|
||||
|
||||
+81
-77
@@ -1,83 +1,13 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('HELP_CENTER.CATEGORY.EDIT.TITLE')"
|
||||
:header-content="$t('HELP_CENTER.CATEGORY.EDIT.SUB_TITLE')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="onUpdate">
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row w-full mt-0 mx-0 mb-4">
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.EDIT.PORTAL') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ portalName }}</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.EDIT.LOCALE') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ locale }}</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<category-name-icon-input
|
||||
:label="$t('HELP_CENTER.CATEGORY.EDIT.NAME.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.EDIT.NAME.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.EDIT.NAME.HELP_TEXT')"
|
||||
:has-error="$v.name.$error"
|
||||
:error-message="$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR')"
|
||||
:existing-name="category.name"
|
||||
:saved-icon="category.icon"
|
||||
@name-change="changeName"
|
||||
@icon-change="onClickInsertEmoji"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="slug"
|
||||
:class="{ error: $v.slug.$error }"
|
||||
class="w-full"
|
||||
:error="slugError"
|
||||
:label="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.HELP_TEXT')"
|
||||
@input="$v.slug.$touch"
|
||||
/>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.CATEGORY.EDIT.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button @click="editCategory">
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
import CategoryNameIconInput from './NameEmojiInput.vue';
|
||||
|
||||
export default {
|
||||
components: { CategoryNameIconInput },
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -100,6 +30,9 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: this.category.id,
|
||||
@@ -120,7 +53,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
slugError() {
|
||||
if (this.$v.slug.$error) {
|
||||
if (this.v$.slug.$error) {
|
||||
return this.$t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
|
||||
}
|
||||
return '';
|
||||
@@ -159,8 +92,8 @@ export default {
|
||||
slug,
|
||||
description,
|
||||
};
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -180,12 +113,83 @@ export default {
|
||||
errorMessage ||
|
||||
this.$t('HELP_CENTER.CATEGORY.EDIT.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('HELP_CENTER.CATEGORY.EDIT.TITLE')"
|
||||
:header-content="$t('HELP_CENTER.CATEGORY.EDIT.SUB_TITLE')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="onUpdate">
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row w-full mx-0 mt-0 mb-4">
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.EDIT.PORTAL') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ portalName }}</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.EDIT.LOCALE') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ locale }}</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<CategoryNameIconInput
|
||||
:label="$t('HELP_CENTER.CATEGORY.EDIT.NAME.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.EDIT.NAME.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.EDIT.NAME.HELP_TEXT')"
|
||||
:has-error="v$.name.$error"
|
||||
:error-message="$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR')"
|
||||
:existing-name="category.name"
|
||||
:saved-icon="category.icon"
|
||||
@nameChange="changeName"
|
||||
@iconChange="onClickInsertEmoji"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="slug"
|
||||
:class="{ error: v$.slug.$error }"
|
||||
class="w-full"
|
||||
:error="slugError"
|
||||
:label="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.HELP_TEXT')"
|
||||
@input="v$.slug.$touch"
|
||||
/>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.CATEGORY.EDIT.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button @click="editCategory">
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article-info {
|
||||
width: 100%;
|
||||
|
||||
+3
-3
@@ -134,13 +134,13 @@ function changeCurrentCategory(event) {
|
||||
</div>
|
||||
</header>
|
||||
<div class="category-list">
|
||||
<category-list-item
|
||||
<CategoryListItem
|
||||
:categories="categoriesByLocaleCode"
|
||||
@delete="deleteCategory"
|
||||
@edit="openEditCategoryModal"
|
||||
/>
|
||||
</div>
|
||||
<edit-category
|
||||
<EditCategory
|
||||
v-if="showEditCategoryModal"
|
||||
:show.sync="showEditCategoryModal"
|
||||
:portal-name="currentPortalName"
|
||||
@@ -149,7 +149,7 @@ function changeCurrentCategory(event) {
|
||||
:selected-portal-slug="currentPortalSlug"
|
||||
@cancel="closeEditCategoryModal"
|
||||
/>
|
||||
<add-category
|
||||
<AddCategory
|
||||
v-if="showAddCategoryModal"
|
||||
:show.sync="showAddCategoryModal"
|
||||
:portal-name="currentPortalName"
|
||||
|
||||
+40
-40
@@ -1,42 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-center relative">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
class="absolute [&>span]:flex [&>span]:items-center [&>span]:justify-center z-10 top-[28px] h-[2.5rem] w-[2.45rem] !text-slate-400 dark:!text-slate-600 dark:!bg-slate-900 !p-0"
|
||||
color-scheme="secondary"
|
||||
@click="toggleEmojiPicker"
|
||||
>
|
||||
<span v-if="icon" v-dompurify-html="icon" class="text-lg" />
|
||||
<fluent-icon
|
||||
v-else
|
||||
size="18"
|
||||
icon="emoji-add"
|
||||
type="outline"
|
||||
class="text-slate-400 dark:text-slate-600"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-input
|
||||
v-model="name"
|
||||
:class="{ error: hasError }"
|
||||
class="!mt-0 !mb-4 !mx-0 [&>input]:!mb-0 ltr:[&>input]:ml-12 rtl:[&>input]:mr-12 relative w-[calc(100%-3rem)] [&>p]:w-max"
|
||||
:error="nameErrorMessage"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:help-text="helpText"
|
||||
@input="onNameChange"
|
||||
/>
|
||||
<emoji-input
|
||||
v-if="showEmojiPicker"
|
||||
v-on-clickaway="hideEmojiPicker"
|
||||
class="left-0 top-16"
|
||||
:show-remove-button="true"
|
||||
:on-click="onClickInsertEmoji"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
|
||||
|
||||
export default {
|
||||
components: { EmojiInput },
|
||||
@@ -98,11 +61,11 @@ export default {
|
||||
},
|
||||
onClickInsertEmoji(emoji = '') {
|
||||
this.icon = emoji;
|
||||
this.$emit('icon-change', emoji);
|
||||
this.$emit('iconChange', emoji);
|
||||
this.showEmojiPicker = false;
|
||||
},
|
||||
onNameChange() {
|
||||
this.$emit('name-change', this.name);
|
||||
this.$emit('nameChange', this.name);
|
||||
},
|
||||
hideEmojiPicker() {
|
||||
if (this.showEmojiPicker) {
|
||||
@@ -113,6 +76,43 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex items-center">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
class="absolute [&>span]:flex [&>span]:items-center [&>span]:justify-center z-10 top-[28px] h-[2.5rem] w-[2.45rem] !text-slate-400 dark:!text-slate-600 dark:!bg-slate-900 !p-0"
|
||||
color-scheme="secondary"
|
||||
@click="toggleEmojiPicker"
|
||||
>
|
||||
<span v-if="icon" v-dompurify-html="icon" class="text-lg" />
|
||||
<fluent-icon
|
||||
v-else
|
||||
size="18"
|
||||
icon="emoji-add"
|
||||
type="outline"
|
||||
class="text-slate-400 dark:text-slate-600"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-input
|
||||
v-model="name"
|
||||
:class="{ error: hasError }"
|
||||
class="!mt-0 !mb-4 !mx-0 [&>input]:!mb-0 ltr:[&>input]:ml-12 rtl:[&>input]:mr-12 relative w-[calc(100%-3rem)] [&>p]:w-max"
|
||||
:error="nameErrorMessage"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:help-text="helpText"
|
||||
@input="onNameChange"
|
||||
/>
|
||||
<EmojiInput
|
||||
v-if="showEmojiPicker"
|
||||
v-on-clickaway="hideEmojiPicker"
|
||||
class="left-0 top-16"
|
||||
show-remove-button
|
||||
:on-click="onClickInsertEmoji"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.emoji-dialog::before {
|
||||
@apply hidden;
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<template>
|
||||
<div>Component to create a category</div>
|
||||
<div>{{ 'Component to create a category' }}</div>
|
||||
</template>
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<template>
|
||||
<div>Component to show details of a category</div>
|
||||
<div>{{ 'Component to show details of a category' }}</div>
|
||||
</template>
|
||||
|
||||
@@ -1,38 +1,4 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<settings-header
|
||||
button-route="new"
|
||||
:header-title="$t('HELP_CENTER.PORTAL.EDIT.HEADER_TEXT')"
|
||||
show-back-button
|
||||
:back-button-label="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BACK_BUTTON')
|
||||
"
|
||||
:show-new-button="false"
|
||||
/>
|
||||
<div class="overflow-auto max-h-[96%]">
|
||||
<setting-intro-banner :header-title="portalName">
|
||||
<woot-tabs
|
||||
:index="activeTabIndex"
|
||||
:border="false"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</setting-intro-banner>
|
||||
<div class="flex flex-wrap max-w-full px-8 py-4 my-auto">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import SettingsHeader from 'dashboard/routes/dashboard/settings/SettingsHeader.vue';
|
||||
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner.vue';
|
||||
@@ -44,9 +10,6 @@ export default {
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
currentPortal() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
if (slug) return this.$store.getters['portals/portalBySlug'](slug);
|
||||
@@ -99,6 +62,40 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<SettingsHeader
|
||||
button-route="new"
|
||||
:header-title="$t('HELP_CENTER.PORTAL.EDIT.HEADER_TEXT')"
|
||||
show-back-button
|
||||
:back-button-label="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BACK_BUTTON')
|
||||
"
|
||||
:show-new-button="false"
|
||||
/>
|
||||
<div class="overflow-auto max-h-[96%]">
|
||||
<SettingIntroBanner :header-title="portalName">
|
||||
<woot-tabs
|
||||
:index="activeTabIndex"
|
||||
:border="false"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</SettingIntroBanner>
|
||||
<div class="flex flex-wrap max-w-full px-8 py-4 my-auto">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wrapper {
|
||||
flex: 1;
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ async function deleteLogo() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<portal-settings-basic-form
|
||||
<PortalSettingsBasicForm
|
||||
v-if="currentPortal"
|
||||
:portal="currentPortal"
|
||||
:is-submitting="uiFlags.isUpdating"
|
||||
@@ -79,6 +79,6 @@ async function deleteLogo() {
|
||||
$t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT')
|
||||
"
|
||||
@submit="updatePortalSettings"
|
||||
@delete-logo="deleteLogo"
|
||||
@deleteLogo="deleteLogo"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ async function updatePortalSettings(portalObj) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<portal-settings-customization-form
|
||||
<PortalSettingsCustomizationForm
|
||||
v-if="currentPortal"
|
||||
:portal="currentPortal"
|
||||
:is-submitting="uiFlags.isUpdating"
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ function addLocale() {
|
||||
v-if="currentPortal"
|
||||
:locales="locales"
|
||||
:selected-locale-code="currentPortal.meta.default_locale"
|
||||
@change-default-locale="changeDefaultLocale"
|
||||
@changeDefaultLocale="changeDefaultLocale"
|
||||
@delete="deletePortalLocale"
|
||||
/>
|
||||
<woot-modal
|
||||
|
||||
+54
-57
@@ -1,60 +1,5 @@
|
||||
<template>
|
||||
<div class="py-2 px-4 w-full max-w-full">
|
||||
<div class="flex justify-between items-center mt-0 mb-2 mx-0 h-12">
|
||||
<div class="flex items-center">
|
||||
<woot-sidemenu-icon />
|
||||
<h1
|
||||
class="my-0 mx-2 text-2xl text-slate-800 dark:text-slate-100 font-medium"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.HEADER') }}
|
||||
</h1>
|
||||
</div>
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
icon="add"
|
||||
size="small"
|
||||
@click="addPortal"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.NEW_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="h-[90vh] overflow-y-scroll">
|
||||
<portal-list-item
|
||||
v-for="portal in portals"
|
||||
:key="portal.id"
|
||||
:portal="portal"
|
||||
:status="portalStatus"
|
||||
@add-locale="addLocale"
|
||||
@open-site="openPortal"
|
||||
/>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="items-center flex text-base justify-center p-40"
|
||||
>
|
||||
<spinner />
|
||||
<span>{{ $t('HELP_CENTER.PORTAL.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
<empty-state
|
||||
v-else-if="shouldShowEmptyState"
|
||||
:title="$t('HELP_CENTER.PORTAL.NO_PORTALS_MESSAGE')"
|
||||
/>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show.sync="isAddLocaleModalOpen"
|
||||
:on-close="closeAddLocaleModal"
|
||||
>
|
||||
<add-locale
|
||||
:show="isAddLocaleModalOpen"
|
||||
:portal="selectedPortal"
|
||||
@cancel="closeAddLocaleModal"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import PortalListItem from '../../components/PortalListItem.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
@@ -68,7 +13,6 @@ export default {
|
||||
Spinner,
|
||||
AddLocale,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
isAddLocaleModalOpen: false,
|
||||
@@ -78,7 +22,6 @@ export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
portals: 'portals/allPortals',
|
||||
meta: 'portals/getMeta',
|
||||
isFetching: 'portals/isFetchingPortals',
|
||||
}),
|
||||
portalStatus() {
|
||||
@@ -106,3 +49,57 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-full px-4 py-2">
|
||||
<div class="flex items-center justify-between h-12 mx-0 mt-0 mb-2">
|
||||
<div class="flex items-center">
|
||||
<woot-sidemenu-icon />
|
||||
<h1
|
||||
class="mx-2 my-0 text-2xl font-medium text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.HEADER') }}
|
||||
</h1>
|
||||
</div>
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
icon="add"
|
||||
size="small"
|
||||
@click="addPortal"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.NEW_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="h-[90vh] overflow-y-scroll">
|
||||
<PortalListItem
|
||||
v-for="portal in portals"
|
||||
:key="portal.id"
|
||||
:portal="portal"
|
||||
:status="portalStatus"
|
||||
@addLocale="addLocale"
|
||||
@openSite="openPortal"
|
||||
/>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center p-40 text-base"
|
||||
>
|
||||
<Spinner />
|
||||
<span>{{ $t('HELP_CENTER.PORTAL.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else-if="shouldShowEmptyState"
|
||||
:title="$t('HELP_CENTER.PORTAL.NO_PORTALS_MESSAGE')"
|
||||
/>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show.sync="isAddLocaleModalOpen"
|
||||
:on-close="closeAddLocaleModal"
|
||||
>
|
||||
<AddLocale
|
||||
:show="isAddLocaleModalOpen"
|
||||
:portal="selectedPortal"
|
||||
@cancel="closeAddLocaleModal"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,31 +1,3 @@
|
||||
<template>
|
||||
<section class="flex-1">
|
||||
<settings-header
|
||||
button-route="new"
|
||||
:header-title="portalHeaderText"
|
||||
show-back-button
|
||||
:back-button-label="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BACK_BUTTON')
|
||||
"
|
||||
:show-new-button="false"
|
||||
/>
|
||||
<div
|
||||
class="grid grid-cols-[20rem_1fr] w-full h-full overflow-auto rtl:pl-0 rtl:pr-4 bg-slate-50 dark:bg-slate-800 p-5"
|
||||
>
|
||||
<woot-wizard
|
||||
class="hidden md:block"
|
||||
:global-config="globalConfig"
|
||||
:items="items"
|
||||
/>
|
||||
<div
|
||||
class="w-full p-5 bg-white border border-transparent border-solid rounded-md shadow-sm dark:bg-slate-900 dark:border-transparent"
|
||||
>
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
@@ -68,3 +40,31 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex-1">
|
||||
<SettingsHeader
|
||||
button-route="new"
|
||||
:header-title="portalHeaderText"
|
||||
show-back-button
|
||||
:back-button-label="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BACK_BUTTON')
|
||||
"
|
||||
:show-new-button="false"
|
||||
/>
|
||||
<div
|
||||
class="grid grid-cols-[20rem_1fr] w-full h-full overflow-auto rtl:pl-0 rtl:pr-4 bg-slate-50 dark:bg-slate-800 p-5"
|
||||
>
|
||||
<woot-wizard
|
||||
class="hidden md:block"
|
||||
:global-config="globalConfig"
|
||||
:items="items"
|
||||
/>
|
||||
<div
|
||||
class="w-full p-5 bg-white border border-transparent border-solid rounded-md shadow-sm dark:bg-slate-900 dark:border-transparent"
|
||||
>
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ async function updatePortalSettings(portalObj) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<portal-settings-customization-form
|
||||
<PortalSettingsCustomizationForm
|
||||
v-if="currentPortal"
|
||||
:portal="currentPortal"
|
||||
:is-submitting="uiFlags.isUpdating"
|
||||
|
||||
+14
-15
@@ -1,18 +1,6 @@
|
||||
<template>
|
||||
<PortalSettingsBasicForm
|
||||
:is-submitting="uiFlags.isCreating"
|
||||
:submit-button-text="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.CREATE_BASIC_SETTING_BUTTON'
|
||||
)
|
||||
"
|
||||
@submit="createPortal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import PortalSettingsBasicForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
@@ -21,7 +9,6 @@ export default {
|
||||
components: {
|
||||
PortalSettingsBasicForm,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
@@ -63,9 +50,21 @@ export default {
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_BASIC');
|
||||
} finally {
|
||||
this.showAlert(this.alertMessage);
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PortalSettingsBasicForm
|
||||
:is-submitting="uiFlags.isCreating"
|
||||
:submit-button-text="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.CREATE_BASIC_SETTING_BUTTON'
|
||||
)
|
||||
"
|
||||
@submit="createPortal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
+10
-10
@@ -1,8 +1,16 @@
|
||||
<script setup>
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
defineComponent({
|
||||
name: 'PortalSettingsFinish',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 w-full h-full max-w-full px-6 pt-3 pb-6 bg-white border border-transparent border-solid dark:bg-slate-900 dark:border-transparent"
|
||||
>
|
||||
<empty-state
|
||||
<EmptyState
|
||||
:title="$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.FINISH_PAGE.TITLE')"
|
||||
:message="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.FINISH_PAGE.MESSAGE')
|
||||
@@ -18,14 +26,6 @@
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.FINISH_PAGE.FINISH') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</empty-state>
|
||||
</EmptyState>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
defineComponent({
|
||||
name: 'PortalSettingsFinish',
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<template>
|
||||
<div>Component to view details of portal</div>
|
||||
<div>{{ 'Component to view details of portal' }}</div>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="text-center bg-slate-25 dark:bg-slate-800 justify-center w-full h-full hidden md:flex items-center"
|
||||
>
|
||||
<span v-if="uiFlags.isFetching" class="spinner my-4" />
|
||||
<div v-else class="flex flex-col items-center gap-2">
|
||||
<fluent-icon
|
||||
icon="mail-inbox"
|
||||
size="40"
|
||||
class="text-slate-600 dark:text-slate-400"
|
||||
/>
|
||||
<span class="text-slate-500 text-sm font-medium dark:text-slate-300">
|
||||
{{ emptyMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
@@ -38,3 +20,21 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="text-center bg-slate-25 dark:bg-slate-800 justify-center w-full h-full hidden md:flex items-center"
|
||||
>
|
||||
<span v-if="uiFlags.isFetching" class="spinner my-4" />
|
||||
<div v-else class="flex flex-col items-center gap-2">
|
||||
<fluent-icon
|
||||
icon="mail-inbox"
|
||||
size="40"
|
||||
class="text-slate-600 dark:text-slate-400"
|
||||
/>
|
||||
<span class="text-slate-500 text-sm font-medium dark:text-slate-300">
|
||||
{{ emptyMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,67 +1,29 @@
|
||||
<template>
|
||||
<section class="flex w-full h-full bg-white dark:bg-slate-900">
|
||||
<div
|
||||
class="flex flex-col h-full w-full md:min-w-[360px] md:max-w-[360px] ltr:border-r border-slate-50 dark:border-slate-800/50"
|
||||
:class="!currentNotificationId ? 'flex' : 'hidden md:flex'"
|
||||
>
|
||||
<inbox-list-header
|
||||
:is-context-menu-open="isInboxContextMenuOpen"
|
||||
@filter="onFilterChange"
|
||||
@redirect="redirectToInbox"
|
||||
/>
|
||||
<div
|
||||
ref="notificationList"
|
||||
class="flex flex-col w-full h-[calc(100%-56px)] overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
<inbox-card
|
||||
v-for="notificationItem in notifications"
|
||||
:key="notificationItem.id"
|
||||
:active="currentNotificationId === notificationItem.id"
|
||||
:notification-item="notificationItem"
|
||||
@mark-notification-as-read="markNotificationAsRead"
|
||||
@mark-notification-as-unread="markNotificationAsUnRead"
|
||||
@delete-notification="deleteNotification"
|
||||
@context-menu-open="isInboxContextMenuOpen = true"
|
||||
@context-menu-close="isInboxContextMenuOpen = false"
|
||||
/>
|
||||
<div v-if="uiFlags.isFetching" class="text-center">
|
||||
<span class="spinner mt-4 mb-4" />
|
||||
</div>
|
||||
<p
|
||||
v-if="showEmptyState"
|
||||
class="text-center text-slate-400 text-sm dark:text-slate-400 p-4 font-medium"
|
||||
>
|
||||
{{ $t('INBOX.LIST.NO_NOTIFICATIONS') }}
|
||||
</p>
|
||||
<intersection-observer
|
||||
v-if="!showEndOfList && !uiFlags.isFetching"
|
||||
:options="infiniteLoaderOptions"
|
||||
@observed="loadMoreNotifications"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<router-view />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import InboxCard from './components/InboxCard.vue';
|
||||
import InboxListHeader from './components/InboxListHeader.vue';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxCard,
|
||||
InboxListHeader,
|
||||
IntersectionObserver,
|
||||
CmdBarConversationSnooze,
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
};
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
infiniteLoaderOptions: {
|
||||
@@ -78,7 +40,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
meta: 'notifications/getMeta',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
notification: 'notifications/getFilteredNotifications',
|
||||
@@ -151,7 +112,7 @@ export default {
|
||||
unreadCount: this.meta.unreadCount,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
|
||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
|
||||
});
|
||||
},
|
||||
markNotificationAsUnRead(notification) {
|
||||
@@ -163,7 +124,7 @@ export default {
|
||||
id,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
||||
});
|
||||
},
|
||||
deleteNotification(notification) {
|
||||
@@ -176,7 +137,7 @@ export default {
|
||||
count: this.meta.count,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('INBOX.ALERTS.DELETE'));
|
||||
useAlert(this.$t('INBOX.ALERTS.DELETE'));
|
||||
});
|
||||
},
|
||||
onFilterChange(option) {
|
||||
@@ -206,3 +167,50 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex w-full h-full bg-white dark:bg-slate-900">
|
||||
<div
|
||||
class="flex flex-col h-full w-full md:min-w-[360px] md:max-w-[360px] ltr:border-r border-slate-50 dark:border-slate-800/50"
|
||||
:class="!currentNotificationId ? 'flex' : 'hidden md:flex'"
|
||||
>
|
||||
<InboxListHeader
|
||||
:is-context-menu-open="isInboxContextMenuOpen"
|
||||
@filter="onFilterChange"
|
||||
@redirect="redirectToInbox"
|
||||
/>
|
||||
<div
|
||||
ref="notificationList"
|
||||
class="flex flex-col w-full h-[calc(100%-56px)] overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
<InboxCard
|
||||
v-for="notificationItem in notifications"
|
||||
:key="notificationItem.id"
|
||||
:active="currentNotificationId === notificationItem.id"
|
||||
:notification-item="notificationItem"
|
||||
@markNotificationAsRead="markNotificationAsRead"
|
||||
@markNotificationAsUnRead="markNotificationAsUnRead"
|
||||
@deleteNotification="deleteNotification"
|
||||
@contextMenuOpen="isInboxContextMenuOpen = true"
|
||||
@contextMenuClose="isInboxContextMenuOpen = false"
|
||||
/>
|
||||
<div v-if="uiFlags.isFetching" class="text-center">
|
||||
<span class="mt-4 mb-4 spinner" />
|
||||
</div>
|
||||
<p
|
||||
v-if="showEmptyState"
|
||||
class="p-4 text-sm font-medium text-center text-slate-400 dark:text-slate-400"
|
||||
>
|
||||
{{ $t('INBOX.LIST.NO_NOTIFICATIONS') }}
|
||||
</p>
|
||||
<IntersectionObserver
|
||||
v-if="!showEndOfList && !uiFlags.isFetching"
|
||||
:options="infiniteLoaderOptions"
|
||||
@observed="loadMoreNotifications"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<router-view />
|
||||
<CmdBarConversationSnooze />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,44 +1,9 @@
|
||||
<template>
|
||||
<div class="h-full w-full md:w-[calc(100%-360px)]">
|
||||
<div v-if="showEmptyState" class="flex w-full h-full">
|
||||
<inbox-empty-state
|
||||
:empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex flex-col h-full w-full">
|
||||
<inbox-item-header
|
||||
class="flex-1"
|
||||
:total-length="totalNotificationCount"
|
||||
:current-index="activeNotificationIndex"
|
||||
:active-notification="activeNotification"
|
||||
@next="onClickNext"
|
||||
@prev="onClickPrev"
|
||||
/>
|
||||
<div
|
||||
v-if="isConversationLoading"
|
||||
class="flex items-center h-[calc(100%-56px)] justify-center bg-slate-25 dark:bg-slate-800"
|
||||
>
|
||||
<span class="spinner my-4" />
|
||||
</div>
|
||||
<conversation-box
|
||||
v-else
|
||||
class="h-[calc(100%-56px)]"
|
||||
is-inbox-view
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="false"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import InboxItemHeader from './components/InboxItemHeader.vue';
|
||||
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue';
|
||||
import InboxEmptyState from './InboxEmptyState.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -48,7 +13,14 @@ export default {
|
||||
InboxEmptyState,
|
||||
ConversationBox,
|
||||
},
|
||||
mixins: [uiSettingsMixin],
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isConversationLoading: false,
|
||||
@@ -56,7 +28,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
notification: 'notifications/getFilteredNotifications',
|
||||
currentChat: 'getSelectedChat',
|
||||
activeNotificationById: 'notifications/getNotificationById',
|
||||
@@ -200,3 +171,38 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full md:w-[calc(100%-360px)]">
|
||||
<div v-if="showEmptyState" class="flex w-full h-full">
|
||||
<InboxEmptyState
|
||||
:empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex flex-col w-full h-full">
|
||||
<InboxItemHeader
|
||||
class="flex-1"
|
||||
:total-length="totalNotificationCount"
|
||||
:current-index="activeNotificationIndex"
|
||||
:active-notification="activeNotification"
|
||||
@next="onClickNext"
|
||||
@prev="onClickPrev"
|
||||
/>
|
||||
<div
|
||||
v-if="isConversationLoading"
|
||||
class="flex items-center h-[calc(100%-56px)] justify-center bg-slate-25 dark:bg-slate-800"
|
||||
>
|
||||
<span class="my-4 spinner" />
|
||||
</div>
|
||||
<ConversationBox
|
||||
v-else
|
||||
class="h-[calc(100%-56px)]"
|
||||
is-inbox-view
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="false"
|
||||
@contactPanelToggle="onToggleContactPanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,69 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
role="button"
|
||||
class="flex flex-col ltr:pl-5 rtl:pl-3 rtl:pr-5 ltr:pr-3 gap-2.5 py-3 w-full border-b border-slate-50 dark:border-slate-800/50 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
|
||||
:class="
|
||||
active
|
||||
? 'bg-slate-25 dark:bg-slate-800 click-animation'
|
||||
: 'bg-white dark:bg-slate-900'
|
||||
"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
@click="openConversation(notificationItem)"
|
||||
>
|
||||
<div class="flex relative items-center justify-between w-full">
|
||||
<div
|
||||
v-if="isUnread"
|
||||
class="absolute ltr:-left-3.5 rtl:-right-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
||||
/>
|
||||
<InboxNameAndId :inbox="inbox" :conversation-id="primaryActor.id" />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<PriorityIcon :priority="primaryActor.priority" />
|
||||
<StatusIcon :status="primaryActor.status" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between items-center w-full gap-2">
|
||||
<Thumbnail
|
||||
v-if="assigneeMeta"
|
||||
:src="assigneeMeta.thumbnail"
|
||||
:username="assigneeMeta.name"
|
||||
size="16px"
|
||||
/>
|
||||
<span
|
||||
class="flex-1 text-slate-800 dark:text-slate-50 text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
:class="isUnread ? 'font-medium' : 'font-normal'"
|
||||
>
|
||||
{{ pushTitle }}
|
||||
</span>
|
||||
<span
|
||||
class="font-medium text-slate-600 dark:text-slate-300 text-xs whitespace-nowrap"
|
||||
>
|
||||
{{ lastActivityAt }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="snoozedUntilTime" class="flex items-center">
|
||||
<span class="text-woot-500 dark:text-woot-500 text-xs font-medium">
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
</div>
|
||||
<inbox-context-menu
|
||||
v-if="isContextMenuOpen"
|
||||
:context-menu-position="contextMenuPosition"
|
||||
:menu-items="menuItems"
|
||||
@close="closeContextMenu"
|
||||
@click="handleAction"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PriorityIcon from './PriorityIcon.vue';
|
||||
import StatusIcon from './StatusIcon.vue';
|
||||
import InboxNameAndId from './InboxNameAndId.vue';
|
||||
import InboxContextMenu from './InboxContextMenu.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime, shortTimestamp } from 'shared/helpers/timeHelper';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
export default {
|
||||
@@ -74,7 +15,6 @@ export default {
|
||||
InboxNameAndId,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
@@ -115,10 +55,8 @@ export default {
|
||||
);
|
||||
},
|
||||
lastActivityAt() {
|
||||
const dynamicTime = this.dynamicTime(
|
||||
this.notificationItem?.last_activity_at
|
||||
);
|
||||
return this.shortTimestamp(dynamicTime, true);
|
||||
const time = dynamicTime(this.notificationItem?.last_activity_at);
|
||||
return shortTimestamp(time, true);
|
||||
},
|
||||
menuItems() {
|
||||
const items = [
|
||||
@@ -188,7 +126,7 @@ export default {
|
||||
closeContextMenu() {
|
||||
this.isContextMenuOpen = false;
|
||||
this.contextMenuPosition = { x: null, y: null };
|
||||
this.$emit('context-menu-close');
|
||||
this.$emit('contextMenuClose');
|
||||
},
|
||||
openContextMenu(e) {
|
||||
this.closeContextMenu();
|
||||
@@ -198,18 +136,18 @@ export default {
|
||||
y: e.pageY || e.clientY,
|
||||
};
|
||||
this.isContextMenuOpen = true;
|
||||
this.$emit('context-menu-open');
|
||||
this.$emit('contextMenuOpen');
|
||||
},
|
||||
handleAction(key) {
|
||||
switch (key) {
|
||||
case 'mark_as_read':
|
||||
this.$emit('mark-notification-as-read', this.notificationItem);
|
||||
this.$emit('markNotificationAsRead', this.notificationItem);
|
||||
break;
|
||||
case 'mark_as_unread':
|
||||
this.$emit('mark-notification-as-unread', this.notificationItem);
|
||||
this.$emit('markNotificationAsUnRead', this.notificationItem);
|
||||
break;
|
||||
case 'delete':
|
||||
this.$emit('delete-notification', this.notificationItem);
|
||||
this.$emit('deleteNotification', this.notificationItem);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
@@ -218,6 +156,65 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="button"
|
||||
class="flex flex-col ltr:pl-5 rtl:pl-3 rtl:pr-5 ltr:pr-3 gap-2.5 py-3 w-full border-b border-slate-50 dark:border-slate-800/50 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
|
||||
:class="
|
||||
active
|
||||
? 'bg-slate-25 dark:bg-slate-800 click-animation'
|
||||
: 'bg-white dark:bg-slate-900'
|
||||
"
|
||||
@contextmenu="openContextMenu($event)"
|
||||
@click="openConversation(notificationItem)"
|
||||
>
|
||||
<div class="relative flex items-center justify-between w-full">
|
||||
<div
|
||||
v-if="isUnread"
|
||||
class="absolute ltr:-left-3.5 rtl:-right-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
||||
/>
|
||||
<InboxNameAndId :inbox="inbox" :conversation-id="primaryActor.id" />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<PriorityIcon :priority="primaryActor.priority" />
|
||||
<StatusIcon :status="primaryActor.status" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-center justify-between w-full gap-2">
|
||||
<Thumbnail
|
||||
v-if="assigneeMeta"
|
||||
:src="assigneeMeta.thumbnail"
|
||||
:username="assigneeMeta.name"
|
||||
size="16px"
|
||||
/>
|
||||
<span
|
||||
class="flex-1 overflow-hidden text-sm text-slate-800 dark:text-slate-50 text-ellipsis whitespace-nowrap"
|
||||
:class="isUnread ? 'font-medium' : 'font-normal'"
|
||||
>
|
||||
{{ pushTitle }}
|
||||
</span>
|
||||
<span
|
||||
class="text-xs font-medium text-slate-600 dark:text-slate-300 whitespace-nowrap"
|
||||
>
|
||||
{{ lastActivityAt }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="snoozedUntilTime" class="flex items-center">
|
||||
<span class="text-xs font-medium text-woot-500 dark:text-woot-500">
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
</div>
|
||||
<InboxContextMenu
|
||||
v-if="isContextMenuOpen"
|
||||
:context-menu-position="contextMenuPosition"
|
||||
:menu-items="menuItems"
|
||||
@close="closeContextMenu"
|
||||
@click="handleAction"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.click-animation {
|
||||
animation: click-animation 0.3s ease-in-out;
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
<template>
|
||||
<woot-context-menu
|
||||
:x="contextMenuPosition.x"
|
||||
:y="contextMenuPosition.y"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 w-40 py-1 border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl"
|
||||
>
|
||||
<menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
@click="onMenuItemClick(item.key)"
|
||||
/>
|
||||
</div>
|
||||
</woot-context-menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuItem from './MenuItem.vue';
|
||||
export default {
|
||||
@@ -44,3 +25,22 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-context-menu
|
||||
:x="contextMenuPosition.x"
|
||||
:y="contextMenuPosition.y"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 w-40 py-1 border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl"
|
||||
>
|
||||
<MenuItem
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
@click="onMenuItemClick(item.key)"
|
||||
/>
|
||||
</div>
|
||||
</woot-context-menu>
|
||||
</template>
|
||||
|
||||
@@ -1,107 +1,16 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col bg-white z-50 dark:bg-slate-900 w-[170px] border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl divide-y divide-slate-100 dark:divide-slate-700/50"
|
||||
>
|
||||
<div class="flex items-center justify-between h-11 p-3 rounded-t-lg">
|
||||
<div class="flex gap-1.5">
|
||||
<fluent-icon
|
||||
icon="arrow-sort"
|
||||
type="outline"
|
||||
size="16"
|
||||
class="text-slate-700 dark:text-slate-100"
|
||||
/>
|
||||
<span class="font-medium text-xs text-slate-800 dark:text-slate-100">
|
||||
{{ $t('INBOX.DISPLAY_MENU.SORT') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div
|
||||
role="button"
|
||||
class="border h-5 flex gap-1 rounded-md items-center pr-1.5 pl-1 py-0.5 w-[70px] justify-between border-slate-100 dark:border-slate-700/50"
|
||||
@click="openSortMenu"
|
||||
>
|
||||
<span class="text-xs font-medium text-slate-600 dark:text-slate-300">
|
||||
{{ activeSortOption }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
icon="chevron-down"
|
||||
size="12"
|
||||
class="text-slate-600 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showSortMenu"
|
||||
class="absolute flex flex-col gap-0.5 bg-white z-60 dark:bg-slate-800 rounded-md p-0.5 top-0 w-[70px] border border-slate-100 dark:border-slate-700/50"
|
||||
>
|
||||
<div
|
||||
v-for="option in sortOptions"
|
||||
:key="option.key"
|
||||
role="button"
|
||||
class="flex rounded-[4px] h-5 w-full items-center justify-between p-0.5 gap-1"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-woot-700/50': activeSort === option.key,
|
||||
}"
|
||||
@click.stop="onSortOptionClick(option)"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium hover:text-woot-600 dark:hover:text-woot-600"
|
||||
:class="{
|
||||
'text-woot-600 dark:text-woot-600': activeSort === option.key,
|
||||
'text-slate-600 dark:text-slate-300': activeSort !== option.key,
|
||||
}"
|
||||
>
|
||||
{{ option.name }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="activeSort === option.key"
|
||||
icon="checkmark"
|
||||
size="14"
|
||||
class="text-woot-600 dark:text-woot-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="font-medium text-xs py-4 px-3 text-slate-400 dark:text-slate-400"
|
||||
>
|
||||
{{ $t('INBOX.DISPLAY_MENU.DISPLAY') }}
|
||||
</span>
|
||||
<div
|
||||
class="flex flex-col divide-y divide-slate-100 dark:divide-slate-700/50"
|
||||
>
|
||||
<div
|
||||
v-for="option in displayOptions"
|
||||
:key="option.key"
|
||||
class="flex items-center px-3 py-2 gap-1.5 h-9"
|
||||
>
|
||||
<input
|
||||
:id="option.key"
|
||||
type="checkbox"
|
||||
:name="option.key"
|
||||
:checked="option.selected"
|
||||
class="m-0 border-[1.5px] shadow border-slate-200 dark:border-slate-600 appearance-none rounded-[4px] w-4 h-4 dark:bg-slate-800 focus:ring-1 focus:ring-slate-100 dark:focus:ring-slate-700 checked:bg-woot-600 dark:checked:bg-woot-600 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center checked:border-t checked:border-woot-700 dark:checked:border-woot-300 checked:border-b-0 checked:border-r-0 checked:border-l-0 after:text-center after:text-xs after:font-bold after:relative after:-top-[1.5px]"
|
||||
@change="updateDisplayOption(option)"
|
||||
/>
|
||||
<label
|
||||
:for="option.key"
|
||||
class="text-xs font-medium text-slate-800 !ml-0 !mr-0 dark:text-slate-100"
|
||||
>
|
||||
{{ option.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
mixins: [uiSettingsMixin],
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSortMenu: false,
|
||||
@@ -196,3 +105,101 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col bg-white z-50 dark:bg-slate-900 w-[170px] border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl divide-y divide-slate-100 dark:divide-slate-700/50"
|
||||
>
|
||||
<div class="flex items-center justify-between p-3 rounded-t-lg h-11">
|
||||
<div class="flex gap-1.5">
|
||||
<fluent-icon
|
||||
icon="arrow-sort"
|
||||
type="outline"
|
||||
size="16"
|
||||
class="text-slate-700 dark:text-slate-100"
|
||||
/>
|
||||
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">
|
||||
{{ $t('INBOX.DISPLAY_MENU.SORT') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div
|
||||
role="button"
|
||||
class="border h-5 flex gap-1 rounded-md items-center pr-1.5 pl-1 py-0.5 w-[70px] justify-between border-slate-100 dark:border-slate-700/50"
|
||||
@click="openSortMenu"
|
||||
>
|
||||
<span class="text-xs font-medium text-slate-600 dark:text-slate-300">
|
||||
{{ activeSortOption }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
icon="chevron-down"
|
||||
size="12"
|
||||
class="text-slate-600 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showSortMenu"
|
||||
class="absolute flex flex-col gap-0.5 bg-white z-60 dark:bg-slate-800 rounded-md p-0.5 top-0 w-[70px] border border-slate-100 dark:border-slate-700/50"
|
||||
>
|
||||
<div
|
||||
v-for="option in sortOptions"
|
||||
:key="option.key"
|
||||
role="button"
|
||||
class="flex rounded-[4px] h-5 w-full items-center justify-between p-0.5 gap-1"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-woot-700/50': activeSort === option.key,
|
||||
}"
|
||||
@click.stop="onSortOptionClick(option)"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium hover:text-woot-600 dark:hover:text-woot-600"
|
||||
:class="{
|
||||
'text-woot-600 dark:text-woot-600': activeSort === option.key,
|
||||
'text-slate-600 dark:text-slate-300': activeSort !== option.key,
|
||||
}"
|
||||
>
|
||||
{{ option.name }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="activeSort === option.key"
|
||||
icon="checkmark"
|
||||
size="14"
|
||||
class="text-woot-600 dark:text-woot-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="px-3 py-4 text-xs font-medium text-slate-400 dark:text-slate-400"
|
||||
>
|
||||
{{ $t('INBOX.DISPLAY_MENU.DISPLAY') }}
|
||||
</span>
|
||||
<div
|
||||
class="flex flex-col divide-y divide-slate-100 dark:divide-slate-700/50"
|
||||
>
|
||||
<div
|
||||
v-for="option in displayOptions"
|
||||
:key="option.key"
|
||||
class="flex items-center px-3 py-2 gap-1.5 h-9"
|
||||
>
|
||||
<input
|
||||
:id="option.key"
|
||||
type="checkbox"
|
||||
:name="option.key"
|
||||
:checked="option.selected"
|
||||
class="m-0 border-[1.5px] shadow border-slate-200 dark:border-slate-600 appearance-none rounded-[4px] w-4 h-4 dark:bg-slate-800 focus:ring-1 focus:ring-slate-100 dark:focus:ring-slate-700 checked:bg-woot-600 dark:checked:bg-woot-600 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center checked:border-t checked:border-woot-700 dark:checked:border-woot-300 checked:border-b-0 checked:border-r-0 checked:border-l-0 after:text-center after:text-xs after:font-bold after:relative after:-top-[1.5px]"
|
||||
@change="updateDisplayOption(option)"
|
||||
/>
|
||||
<label
|
||||
:for="option.key"
|
||||
class="text-xs font-medium text-slate-800 !ml-0 !mr-0 dark:text-slate-100"
|
||||
>
|
||||
{{ option.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user