fix: apply ESLint rule vue/component-tags-order
This commit is contained in:
@@ -1,44 +1,3 @@
|
||||
<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"
|
||||
@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 flex-1 h-full min-h-0 px-0 overflow-hidden">
|
||||
<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 { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
@@ -181,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"
|
||||
@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 flex-1 h-full min-h-0 px-0 overflow-hidden">
|
||||
<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>
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
<!-- 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';
|
||||
@@ -108,6 +97,17 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ninja-keys
|
||||
ref="ninjakeys"
|
||||
:no-auto-load-md-icons="true"
|
||||
hide-breadcrumbs
|
||||
:placeholder="placeholder"
|
||||
@selected="onSelected"
|
||||
@closed="onClosed"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
ninja-keys {
|
||||
--ninja-accent-color: var(--w-500);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+78
-78
@@ -1,3 +1,81 @@
|
||||
<script>
|
||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
||||
import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations.vue';
|
||||
import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo.vue';
|
||||
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 { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccordionItem,
|
||||
ContactConversations,
|
||||
ContactInfo,
|
||||
ContactLabel,
|
||||
CustomAttributes,
|
||||
draggable,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showCloseButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
contactSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
} = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
contactSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragEnabled: true,
|
||||
contactSidebarItems: [],
|
||||
dragging: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasContactAttributes() {
|
||||
const { custom_attributes: customAttributes } = this.contact;
|
||||
return customAttributes && Object.keys(customAttributes).length;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.contactSidebarItems = this.contactSidebarItemsOrder;
|
||||
},
|
||||
methods: {
|
||||
onDragEnd() {
|
||||
this.dragging = false;
|
||||
this.updateUISettings({
|
||||
contact_sidebar_items_order: this.contactSidebarItems,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</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"
|
||||
@@ -79,84 +157,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
||||
import ContactConversations from 'dashboard/routes/dashboard/conversation/ContactConversations.vue';
|
||||
import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/ContactInfo.vue';
|
||||
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 { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccordionItem,
|
||||
ContactConversations,
|
||||
ContactInfo,
|
||||
ContactLabel,
|
||||
CustomAttributes,
|
||||
draggable,
|
||||
},
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showCloseButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
contactSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
} = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
isContactSidebarItemOpen,
|
||||
contactSidebarItemsOrder,
|
||||
toggleSidebarUIState,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragEnabled: true,
|
||||
contactSidebarItems: [],
|
||||
dragging: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasContactAttributes() {
|
||||
const { custom_attributes: customAttributes } = this.contact;
|
||||
return customAttributes && Object.keys(customAttributes).length;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.contactSidebarItems = this.contactSidebarItemsOrder;
|
||||
},
|
||||
methods: {
|
||||
onDragEnd() {
|
||||
this.dragging = false;
|
||||
this.updateUISettings({
|
||||
contact_sidebar_items_order: this.contactSidebarItems,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.contact--profile {
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
<template>
|
||||
<label-selector
|
||||
:all-labels="allLabels"
|
||||
:saved-labels="savedLabels"
|
||||
@add="addItem"
|
||||
@remove="removeItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -85,4 +76,13 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label-selector
|
||||
:all-labels="allLabels"
|
||||
:saved-labels="savedLabels"
|
||||
@add="addItem"
|
||||
@remove="removeItem"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
|
||||
+90
-90
@@ -1,93 +1,3 @@
|
||||
<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"
|
||||
>
|
||||
<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)"
|
||||
: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>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue';
|
||||
@@ -343,6 +253,96 @@ 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"
|
||||
>
|
||||
<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)"
|
||||
: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="flex-1 h-full -mt-1 overflow-hidden bg-white contacts-table-wrap dark:bg-slate-900"
|
||||
>
|
||||
<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="flex items-center justify-center text-base">
|
||||
<spinner />
|
||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VeTable } from 'vue-easytable';
|
||||
import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
@@ -277,6 +248,35 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex-1 h-full -mt-1 overflow-hidden bg-white contacts-table-wrap dark:bg-slate-900"
|
||||
>
|
||||
<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="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,84 +1,3 @@
|
||||
<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';
|
||||
@@ -468,3 +387,84 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,92 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
segmentsId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreateModal: false,
|
||||
showImportModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
searchButtonClass() {
|
||||
return this.searchQuery !== ''
|
||||
? 'opacity-100 translate-x-0 visible'
|
||||
: '-translate-x-px opacity-0 invisible';
|
||||
},
|
||||
...mapGetters({
|
||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.getAppliedContactFilters.length;
|
||||
},
|
||||
hasActiveSegments() {
|
||||
return this.segmentsId !== 0;
|
||||
},
|
||||
exportDescription() {
|
||||
return this.hasAppliedFilters
|
||||
? this.$t('EXPORT_CONTACTS.CONFIRM.FILTERED_MESSAGE')
|
||||
: this.$t('EXPORT_CONTACTS.CONFIRM.MESSAGE');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onToggleSegmentsModal() {
|
||||
this.$emit('on-toggle-save-filter');
|
||||
},
|
||||
onToggleEditSegmentsModal() {
|
||||
this.$emit('on-toggle-edit-filter');
|
||||
},
|
||||
onToggleDeleteSegmentsModal() {
|
||||
this.$emit('on-toggle-delete-filter');
|
||||
},
|
||||
toggleCreate() {
|
||||
this.$emit('on-toggle-create');
|
||||
},
|
||||
toggleFilter() {
|
||||
this.$emit('on-toggle-filter');
|
||||
},
|
||||
toggleImport() {
|
||||
this.$emit('on-toggle-import');
|
||||
},
|
||||
async submitExport() {
|
||||
const ok =
|
||||
await this.$refs.confirmExportContactsDialog.showConfirmation();
|
||||
|
||||
if (ok) {
|
||||
this.$emit('on-export-submit');
|
||||
}
|
||||
},
|
||||
submitSearch() {
|
||||
this.$emit('on-search-submit');
|
||||
},
|
||||
inputSearch(event) {
|
||||
this.$emit('on-input-search', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="bg-white border-b dark:bg-slate-900 border-slate-50 dark:border-slate-800"
|
||||
@@ -128,92 +217,3 @@
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
searchQuery: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
segmentsId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreateModal: false,
|
||||
showImportModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
searchButtonClass() {
|
||||
return this.searchQuery !== ''
|
||||
? 'opacity-100 translate-x-0 visible'
|
||||
: '-translate-x-px opacity-0 invisible';
|
||||
},
|
||||
...mapGetters({
|
||||
getAppliedContactFilters: 'contacts/getAppliedContactFilters',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.getAppliedContactFilters.length;
|
||||
},
|
||||
hasActiveSegments() {
|
||||
return this.segmentsId !== 0;
|
||||
},
|
||||
exportDescription() {
|
||||
return this.hasAppliedFilters
|
||||
? this.$t('EXPORT_CONTACTS.CONFIRM.FILTERED_MESSAGE')
|
||||
: this.$t('EXPORT_CONTACTS.CONFIRM.MESSAGE');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onToggleSegmentsModal() {
|
||||
this.$emit('on-toggle-save-filter');
|
||||
},
|
||||
onToggleEditSegmentsModal() {
|
||||
this.$emit('on-toggle-edit-filter');
|
||||
},
|
||||
onToggleDeleteSegmentsModal() {
|
||||
this.$emit('on-toggle-delete-filter');
|
||||
},
|
||||
toggleCreate() {
|
||||
this.$emit('on-toggle-create');
|
||||
},
|
||||
toggleFilter() {
|
||||
this.$emit('on-toggle-filter');
|
||||
},
|
||||
toggleImport() {
|
||||
this.$emit('on-toggle-import');
|
||||
},
|
||||
async submitExport() {
|
||||
const ok =
|
||||
await this.$refs.confirmExportContactsDialog.showConfirmation();
|
||||
|
||||
if (ok) {
|
||||
this.$emit('on-export-submit');
|
||||
}
|
||||
},
|
||||
submitSearch() {
|
||||
this.$emit('on-search-submit');
|
||||
},
|
||||
inputSearch(event) {
|
||||
this.$emit('on-input-search', event);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,46 +1,3 @@
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -93,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,32 +1,3 @@
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
export default {
|
||||
@@ -63,6 +34,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;
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
|
||||
<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,3 +1,15 @@
|
||||
<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>
|
||||
|
||||
<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">
|
||||
@@ -13,15 +25,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,136 +1,3 @@
|
||||
<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 { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -266,6 +133,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"
|
||||
>
|
||||
<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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.contact--profile {
|
||||
|
||||
@@ -1,85 +1,4 @@
|
||||
<!-- 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 { useAlert } from 'dashboard/composables';
|
||||
@@ -270,3 +189,84 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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 #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>
|
||||
|
||||
+79
-79
@@ -1,82 +1,3 @@
|
||||
<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 { useAlert } from 'dashboard/composables';
|
||||
@@ -228,6 +149,85 @@ 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="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>
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-pane {
|
||||
@apply box-border top-8 w-full;
|
||||
|
||||
@@ -1,39 +1,3 @@
|
||||
<template>
|
||||
<section class="bg-white conversation-page 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"
|
||||
/>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -254,6 +218,42 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-white conversation-page 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"
|
||||
/>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</section>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.conversation-page {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MacroItem from './MacroItem.vue';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MacroItem,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
macros: ['macros/getMacros'],
|
||||
uiFlags: 'macros/getUIFlags',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('macros/get');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
@@ -32,33 +59,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MacroItem from './MacroItem.vue';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MacroItem,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
macros: ['macros/getMacros'],
|
||||
uiFlags: 'macros/getUIFlags',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('macros/get');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.macros-list {
|
||||
padding: var(--space-smaller);
|
||||
|
||||
@@ -1,37 +1,3 @@
|
||||
<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">
|
||||
<macro-preview
|
||||
v-if="showPreview"
|
||||
v-on-clickaway="closeMacroPreview"
|
||||
:macro="macro"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import MacroPreview from './MacroPreview.vue';
|
||||
@@ -83,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">
|
||||
<macro-preview
|
||||
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,154 +1,3 @@
|
||||
<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>{{ $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>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import {
|
||||
@@ -418,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="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>{{ $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,170 +1,3 @@
|
||||
<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">
|
||||
<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 { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -334,3 +167,170 @@ 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">
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,43 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showCopy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.value);
|
||||
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="w-full h-5 ltr:-ml-1 rtl:-mr-1">
|
||||
<a
|
||||
@@ -56,43 +96,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiOrIcon,
|
||||
},
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emoji: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showCopy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.value);
|
||||
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+239
-239
@@ -1,242 +1,3 @@
|
||||
<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="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-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">
|
||||
<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="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>
|
||||
</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-xs font-medium text-slate-500 ltr:ml-1 rtl:mr-1 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 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>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -561,6 +322,245 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<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="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-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">
|
||||
<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="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>
|
||||
</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-xs font-medium text-slate-500 ltr:ml-1 rtl:mr-1 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 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;
|
||||
|
||||
@@ -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';
|
||||
@@ -54,3 +36,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')"
|
||||
/>
|
||||
<contact-form
|
||||
: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')"
|
||||
/>
|
||||
<contact-form
|
||||
: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')"
|
||||
/>
|
||||
<conversation-form
|
||||
: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
-16
@@ -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';
|
||||
@@ -56,4 +40,20 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<style></style>
|
||||
|
||||
+42
-42
@@ -1,45 +1,3 @@
|
||||
<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 { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -188,6 +146,48 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-attributes--panel {
|
||||
.conversation--attribute {
|
||||
|
||||
@@ -1,48 +1,3 @@
|
||||
<template>
|
||||
<div class="sidebar-labels-wrap">
|
||||
<div
|
||||
v-if="!conversationUiFlags.isFetching"
|
||||
class="contact-conversation--list"
|
||||
>
|
||||
<div
|
||||
v-on-clickaway="closeDropdownLabel"
|
||||
class="label-wrap"
|
||||
@keyup.esc="closeDropdownLabel"
|
||||
>
|
||||
<add-label @add="toggleLabels" />
|
||||
<woot-label
|
||||
v-for="label in activeLabels"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:show-close="true"
|
||||
:color="label.color"
|
||||
variant="smooth"
|
||||
class="max-w-[calc(100%-0.5rem)]"
|
||||
@click="removeLabelFromConversation"
|
||||
/>
|
||||
|
||||
<div class="dropdown-wrap">
|
||||
<div
|
||||
:class="{ 'dropdown-pane--open': showSearchDropdownLabel }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<label-dropdown
|
||||
v-if="showSearchDropdownLabel"
|
||||
:account-labels="accountLabels"
|
||||
:selected-labels="savedLabels"
|
||||
:allow-creation="isAdmin"
|
||||
@add="addLabelToConversation"
|
||||
@remove="removeLabelFromConversation"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<spinner v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
@@ -114,6 +69,51 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-labels-wrap">
|
||||
<div
|
||||
v-if="!conversationUiFlags.isFetching"
|
||||
class="contact-conversation--list"
|
||||
>
|
||||
<div
|
||||
v-on-clickaway="closeDropdownLabel"
|
||||
class="label-wrap"
|
||||
@keyup.esc="closeDropdownLabel"
|
||||
>
|
||||
<add-label @add="toggleLabels" />
|
||||
<woot-label
|
||||
v-for="label in activeLabels"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:show-close="true"
|
||||
:color="label.color"
|
||||
variant="smooth"
|
||||
class="max-w-[calc(100%-0.5rem)]"
|
||||
@click="removeLabelFromConversation"
|
||||
/>
|
||||
|
||||
<div class="dropdown-wrap">
|
||||
<div
|
||||
:class="{ 'dropdown-pane--open': showSearchDropdownLabel }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<label-dropdown
|
||||
v-if="showSearchDropdownLabel"
|
||||
:account-labels="accountLabels"
|
||||
:selected-labels="savedLabels"
|
||||
:allow-creation="isAdmin"
|
||||
@add="addLabelToConversation"
|
||||
@remove="removeLabelFromConversation"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<spinner v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-labels-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -1,37 +1,3 @@
|
||||
<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>
|
||||
<switch-layout
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggle="$emit('toggle-conversation-layout')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
@@ -65,6 +31,40 @@ 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>
|
||||
<switch-layout
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggle="$emit('toggle-conversation-layout')"
|
||||
/>
|
||||
</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,33 +1,3 @@
|
||||
<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 { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
@@ -110,3 +80,33 @@ export default {
|
||||
},
|
||||
};
|
||||
</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,20 +1,3 @@
|
||||
<!-- 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 { useAlert } from 'dashboard/composables';
|
||||
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
@@ -103,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>
|
||||
|
||||
@@ -1,43 +1,3 @@
|
||||
<!-- 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>
|
||||
|
||||
<script>
|
||||
import Modal from 'dashboard/components/Modal.vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
@@ -134,6 +94,46 @@ 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';
|
||||
@@ -79,6 +55,30 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-article--container {
|
||||
@apply my-8 mx-auto py-0 max-w-[56rem] w-full;
|
||||
|
||||
@@ -1,86 +1,3 @@
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
@@ -166,3 +83,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>
|
||||
|
||||
+43
-43
@@ -1,46 +1,3 @@
|
||||
<template>
|
||||
<button
|
||||
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="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="w-full mb-0 text-sm ltr:text-left rtl:text-right text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ locale }}
|
||||
{{ ` / ` }}
|
||||
{{ category || $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.UNCATEGORIZED') }}
|
||||
</p>
|
||||
<div class="flex gap-0.5">
|
||||
<woot-button
|
||||
:title="$t('HELP_CENTER.ARTICLE_SEARCH_RESULT.COPY_LINK')"
|
||||
variant="hollow"
|
||||
color-scheme="secondary"
|
||||
size="tiny"
|
||||
icon="copy"
|
||||
class="invisible group-hover:visible"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
<woot-button
|
||||
class="insert-button"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
size="tiny"
|
||||
@click="handleInsert"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.INSERT_ARTICLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
@@ -90,3 +47,46 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
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="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="w-full mb-0 text-sm ltr:text-left rtl:text-right text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ locale }}
|
||||
{{ ` / ` }}
|
||||
{{ category || $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.UNCATEGORIZED') }}
|
||||
</p>
|
||||
<div class="flex gap-0.5">
|
||||
<woot-button
|
||||
:title="$t('HELP_CENTER.ARTICLE_SEARCH_RESULT.COPY_LINK')"
|
||||
variant="hollow"
|
||||
color-scheme="secondary"
|
||||
size="tiny"
|
||||
icon="copy"
|
||||
class="invisible group-hover:visible"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
<woot-button
|
||||
class="insert-button"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
size="tiny"
|
||||
@click="handleInsert"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.INSERT_ARTICLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
+27
-27
@@ -1,3 +1,30 @@
|
||||
<script>
|
||||
import IframeLoader from 'shared/components/IframeLoader.vue';
|
||||
|
||||
export default {
|
||||
name: 'ArticleView',
|
||||
components: {
|
||||
IframeLoader,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onBack(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('back');
|
||||
},
|
||||
onInsert(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('insert');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col flex-1 overflow-hidden">
|
||||
<div class="py-1">
|
||||
@@ -38,30 +65,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IframeLoader from 'shared/components/IframeLoader.vue';
|
||||
|
||||
export default {
|
||||
name: 'ArticleView',
|
||||
components: {
|
||||
IframeLoader,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onBack(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('back');
|
||||
},
|
||||
onInsert(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('insert');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+35
-35
@@ -1,38 +1,3 @@
|
||||
<template>
|
||||
<div 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 }}
|
||||
</h3>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
@click="onClose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute ltr:left-0 rtl:right-0 w-8 top-0.5 h-8 flex justify-center items-center"
|
||||
>
|
||||
<fluent-icon icon="search" class="" size="18" />
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
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';
|
||||
|
||||
@@ -80,3 +45,38 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div 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 }}
|
||||
</h3>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
@click="onClose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute ltr:left-0 rtl:right-0 w-8 top-0.5 h-8 flex justify-center items-center"
|
||||
>
|
||||
<fluent-icon icon="search" class="" size="18" />
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
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>
|
||||
|
||||
+34
-34
@@ -1,37 +1,3 @@
|
||||
<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]"
|
||||
>
|
||||
<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 { useAlert } from 'dashboard/composables';
|
||||
@@ -161,3 +127,37 @@ export default {
|
||||
},
|
||||
};
|
||||
</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]"
|
||||
>
|
||||
<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>
|
||||
|
||||
+28
-28
@@ -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';
|
||||
|
||||
@@ -67,6 +39,34 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<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';
|
||||
@@ -155,6 +88,73 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<style lang="scss" scoped>
|
||||
/*
|
||||
The .article-ghost-class class is maintained as the vueDraggable doesn't allow multiple classes
|
||||
|
||||
+88
-88
@@ -1,3 +1,91 @@
|
||||
<script>
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
|
||||
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
MultiselectDropdownItems,
|
||||
},
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
selectedValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
shouldShowSettings: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allLocales: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSortByDropdown: false,
|
||||
showLocaleDropdown: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shouldShowLocaleDropdown() {
|
||||
return this.allLocales.length > 1;
|
||||
},
|
||||
switchableLocales() {
|
||||
return this.allLocales.filter(
|
||||
locale => locale.name !== this.selectedLocale
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openFilterModal() {
|
||||
this.$emit('openModal');
|
||||
},
|
||||
openDropdown() {
|
||||
this.$emit('open');
|
||||
this.showSortByDropdown = true;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.$emit('close');
|
||||
this.showSortByDropdown = false;
|
||||
},
|
||||
openLocaleDropdown() {
|
||||
this.showLocaleDropdown = true;
|
||||
},
|
||||
closeLocaleDropdown() {
|
||||
this.showLocaleDropdown = false;
|
||||
},
|
||||
onClickNewArticlePage() {
|
||||
this.$emit('new-article-page');
|
||||
},
|
||||
onClickSelectItem(value) {
|
||||
const { name, code } = value;
|
||||
this.closeLocaleDropdown();
|
||||
if (!name || name === this.selectedLocale) {
|
||||
return;
|
||||
}
|
||||
this.$emit('change-locale', code);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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"
|
||||
@@ -137,94 +225,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
|
||||
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
MultiselectDropdownItems,
|
||||
},
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
selectedValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
shouldShowSettings: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allLocales: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSortByDropdown: false,
|
||||
showLocaleDropdown: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shouldShowLocaleDropdown() {
|
||||
return this.allLocales.length > 1;
|
||||
},
|
||||
switchableLocales() {
|
||||
return this.allLocales.filter(
|
||||
locale => locale.name !== this.selectedLocale
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openFilterModal() {
|
||||
this.$emit('openModal');
|
||||
},
|
||||
openDropdown() {
|
||||
this.$emit('open');
|
||||
this.showSortByDropdown = true;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.$emit('close');
|
||||
this.showSortByDropdown = false;
|
||||
},
|
||||
openLocaleDropdown() {
|
||||
this.showLocaleDropdown = true;
|
||||
},
|
||||
closeLocaleDropdown() {
|
||||
this.showLocaleDropdown = false;
|
||||
},
|
||||
onClickNewArticlePage() {
|
||||
this.$emit('new-article-page');
|
||||
},
|
||||
onClickSelectItem(value) {
|
||||
const { name, code } = value;
|
||||
this.closeLocaleDropdown();
|
||||
if (!name || name === this.selectedLocale) {
|
||||
return;
|
||||
}
|
||||
this.$emit('change-locale', code);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dropdown-pane--open {
|
||||
@apply absolute top-10 right-0 z-50 min-w-[8rem];
|
||||
|
||||
+109
-109
@@ -1,112 +1,3 @@
|
||||
<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 { useAlert } from 'dashboard/composables';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
@@ -232,6 +123,115 @@ 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="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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article--buttons {
|
||||
.dropdown-pane {
|
||||
|
||||
+59
-59
@@ -1,62 +1,3 @@
|
||||
<template>
|
||||
<div class="flex flex-grow-0 w-full h-full min-h-0 app-wrapper">
|
||||
<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 flex-1 h-full min-h-0 px-0 overflow-hidden 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.vue';
|
||||
@@ -342,3 +283,62 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-grow-0 w-full h-full min-h-0 app-wrapper">
|
||||
<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 flex-1 h-full min-h-0 px-0 overflow-hidden 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>
|
||||
|
||||
+187
-187
@@ -1,3 +1,190 @@
|
||||
<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 { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
thumbnail,
|
||||
LocaleItemTable,
|
||||
},
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
values: ['archived', 'draft', 'published'],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
alertMessage: '',
|
||||
selectedPortalForDelete: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
labelColor() {
|
||||
switch (this.status) {
|
||||
case 'Archived':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
},
|
||||
deleteMessageValue() {
|
||||
return ` ${this.selectedPortalForDelete.name}?`;
|
||||
},
|
||||
locales() {
|
||||
return this.portal ? this.portal.config.allowed_locales : [];
|
||||
},
|
||||
allowedLocales() {
|
||||
return Object.keys(this.locales).map(key => {
|
||||
return this.locales[key].code;
|
||||
});
|
||||
},
|
||||
articleCount() {
|
||||
const { allowed_locales: allowedLocales } = this.portal.config;
|
||||
return allowedLocales.reduce((acc, locale) => {
|
||||
return acc + locale.articles_count;
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addLocale() {
|
||||
this.$emit('add-locale', this.portal.id);
|
||||
},
|
||||
openSite() {
|
||||
this.$emit('open-site', this.portal.slug);
|
||||
},
|
||||
openSettings() {
|
||||
this.fetchPortalAndItsCategories();
|
||||
this.navigateToPortalEdit();
|
||||
},
|
||||
onClickOpenDeleteModal(portal) {
|
||||
this.selectedPortalForDelete = portal;
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
async fetchPortalAndItsCategories() {
|
||||
await this.$store.dispatch('portals/index');
|
||||
const {
|
||||
slug,
|
||||
config: { allowed_locales: allowedLocales },
|
||||
} = this.portal;
|
||||
const selectedPortalParam = {
|
||||
portalSlug: slug,
|
||||
locale: allowedLocales[0].code,
|
||||
};
|
||||
this.$store.dispatch('portals/show', selectedPortalParam);
|
||||
this.$store.dispatch('categories/index', selectedPortalParam);
|
||||
},
|
||||
async onClickDeletePortal() {
|
||||
const { slug } = this.selectedPortalForDelete;
|
||||
try {
|
||||
await this.$store.dispatch('portals/delete', {
|
||||
portalSlug: slug,
|
||||
});
|
||||
this.selectedPortalForDelete = {};
|
||||
this.closeDeletePopup();
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS'
|
||||
);
|
||||
this.updateUISettings({
|
||||
last_active_portal_slug: undefined,
|
||||
last_active_locale_code: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_ERROR'
|
||||
);
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
changeDefaultLocale({ localeCode }) {
|
||||
this.updatePortalLocales({
|
||||
allowedLocales: this.allowedLocales,
|
||||
defaultLocale: localeCode,
|
||||
successMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.CHANGE_DEFAULT_LOCALE.API.SUCCESS_MESSAGE'
|
||||
),
|
||||
errorMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.CHANGE_DEFAULT_LOCALE.API.ERROR_MESSAGE'
|
||||
),
|
||||
});
|
||||
this.$track(PORTALS_EVENTS.SET_DEFAULT_LOCALE, {
|
||||
newLocale: localeCode,
|
||||
from: this.$route.name,
|
||||
});
|
||||
},
|
||||
deletePortalLocale({ localeCode }) {
|
||||
const updatedLocales = this.allowedLocales.filter(
|
||||
code => code !== localeCode
|
||||
);
|
||||
const defaultLocale = this.portal.meta.default_locale;
|
||||
this.updatePortalLocales({
|
||||
allowedLocales: updatedLocales,
|
||||
defaultLocale,
|
||||
successMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.DELETE_LOCALE.API.SUCCESS_MESSAGE'
|
||||
),
|
||||
errorMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.DELETE_LOCALE.API.ERROR_MESSAGE'
|
||||
),
|
||||
});
|
||||
this.$track(PORTALS_EVENTS.DELETE_LOCALE, {
|
||||
deletedLocale: localeCode,
|
||||
from: this.$route.name,
|
||||
});
|
||||
},
|
||||
async updatePortalLocales({
|
||||
allowedLocales,
|
||||
defaultLocale,
|
||||
successMessage,
|
||||
errorMessage,
|
||||
}) {
|
||||
try {
|
||||
await this.$store.dispatch('portals/update', {
|
||||
portalSlug: this.portal.slug,
|
||||
config: {
|
||||
default_locale: defaultLocale,
|
||||
allowed_locales: allowedLocales,
|
||||
},
|
||||
});
|
||||
this.alertMessage = successMessage;
|
||||
} catch (error) {
|
||||
this.alertMessage = error?.message || errorMessage;
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
navigateToPortalEdit() {
|
||||
this.$router.push({
|
||||
name: 'edit_portal_information',
|
||||
params: { portalSlug: this.portal.slug },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
@@ -188,190 +375,3 @@
|
||||
/>
|
||||
</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 { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
thumbnail,
|
||||
LocaleItemTable,
|
||||
},
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
values: ['archived', 'draft', 'published'],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
alertMessage: '',
|
||||
selectedPortalForDelete: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
labelColor() {
|
||||
switch (this.status) {
|
||||
case 'Archived':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
},
|
||||
deleteMessageValue() {
|
||||
return ` ${this.selectedPortalForDelete.name}?`;
|
||||
},
|
||||
locales() {
|
||||
return this.portal ? this.portal.config.allowed_locales : [];
|
||||
},
|
||||
allowedLocales() {
|
||||
return Object.keys(this.locales).map(key => {
|
||||
return this.locales[key].code;
|
||||
});
|
||||
},
|
||||
articleCount() {
|
||||
const { allowed_locales: allowedLocales } = this.portal.config;
|
||||
return allowedLocales.reduce((acc, locale) => {
|
||||
return acc + locale.articles_count;
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addLocale() {
|
||||
this.$emit('add-locale', this.portal.id);
|
||||
},
|
||||
openSite() {
|
||||
this.$emit('open-site', this.portal.slug);
|
||||
},
|
||||
openSettings() {
|
||||
this.fetchPortalAndItsCategories();
|
||||
this.navigateToPortalEdit();
|
||||
},
|
||||
onClickOpenDeleteModal(portal) {
|
||||
this.selectedPortalForDelete = portal;
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
async fetchPortalAndItsCategories() {
|
||||
await this.$store.dispatch('portals/index');
|
||||
const {
|
||||
slug,
|
||||
config: { allowed_locales: allowedLocales },
|
||||
} = this.portal;
|
||||
const selectedPortalParam = {
|
||||
portalSlug: slug,
|
||||
locale: allowedLocales[0].code,
|
||||
};
|
||||
this.$store.dispatch('portals/show', selectedPortalParam);
|
||||
this.$store.dispatch('categories/index', selectedPortalParam);
|
||||
},
|
||||
async onClickDeletePortal() {
|
||||
const { slug } = this.selectedPortalForDelete;
|
||||
try {
|
||||
await this.$store.dispatch('portals/delete', {
|
||||
portalSlug: slug,
|
||||
});
|
||||
this.selectedPortalForDelete = {};
|
||||
this.closeDeletePopup();
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS'
|
||||
);
|
||||
this.updateUISettings({
|
||||
last_active_portal_slug: undefined,
|
||||
last_active_locale_code: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_ERROR'
|
||||
);
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
changeDefaultLocale({ localeCode }) {
|
||||
this.updatePortalLocales({
|
||||
allowedLocales: this.allowedLocales,
|
||||
defaultLocale: localeCode,
|
||||
successMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.CHANGE_DEFAULT_LOCALE.API.SUCCESS_MESSAGE'
|
||||
),
|
||||
errorMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.CHANGE_DEFAULT_LOCALE.API.ERROR_MESSAGE'
|
||||
),
|
||||
});
|
||||
this.$track(PORTALS_EVENTS.SET_DEFAULT_LOCALE, {
|
||||
newLocale: localeCode,
|
||||
from: this.$route.name,
|
||||
});
|
||||
},
|
||||
deletePortalLocale({ localeCode }) {
|
||||
const updatedLocales = this.allowedLocales.filter(
|
||||
code => code !== localeCode
|
||||
);
|
||||
const defaultLocale = this.portal.meta.default_locale;
|
||||
this.updatePortalLocales({
|
||||
allowedLocales: updatedLocales,
|
||||
defaultLocale,
|
||||
successMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.DELETE_LOCALE.API.SUCCESS_MESSAGE'
|
||||
),
|
||||
errorMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.DELETE_LOCALE.API.ERROR_MESSAGE'
|
||||
),
|
||||
});
|
||||
this.$track(PORTALS_EVENTS.DELETE_LOCALE, {
|
||||
deletedLocale: localeCode,
|
||||
from: this.$route.name,
|
||||
});
|
||||
},
|
||||
async updatePortalLocales({
|
||||
allowedLocales,
|
||||
defaultLocale,
|
||||
successMessage,
|
||||
errorMessage,
|
||||
}) {
|
||||
try {
|
||||
await this.$store.dispatch('portals/update', {
|
||||
portalSlug: this.portal.slug,
|
||||
config: {
|
||||
default_locale: defaultLocale,
|
||||
allowed_locales: allowedLocales,
|
||||
},
|
||||
});
|
||||
this.alertMessage = successMessage;
|
||||
} catch (error) {
|
||||
this.alertMessage = error?.message || errorMessage;
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
navigateToPortalEdit() {
|
||||
this.$router.push({
|
||||
name: 'edit_portal_information',
|
||||
params: { portalSlug: this.portal.slug },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+26
-26
@@ -1,3 +1,29 @@
|
||||
<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>
|
||||
|
||||
<template>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -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('close-popover');
|
||||
},
|
||||
openPortalPage() {
|
||||
this.closePortalPopover();
|
||||
this.$router.push({
|
||||
name: 'list_all_portals',
|
||||
});
|
||||
},
|
||||
fetchPortalAndItsCategories() {
|
||||
this.$emit('fetch-portal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-on-clickaway="closePortalPopover"
|
||||
@@ -45,41 +83,3 @@
|
||||
</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>
|
||||
|
||||
@@ -1,3 +1,74 @@
|
||||
<script>
|
||||
import thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
thumbnail,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
activePortalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLocale: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locales() {
|
||||
return this.portal?.config?.allowed_locales;
|
||||
},
|
||||
articlesCount() {
|
||||
const { allowed_locales: allowedLocales } = this.portal.config;
|
||||
return allowedLocales.reduce((acc, locale) => {
|
||||
return acc + locale.articles_count;
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.selectedLocale = this.locale || this.portal?.meta?.default_locale;
|
||||
},
|
||||
methods: {
|
||||
onClick(event, code, portal) {
|
||||
event.preventDefault();
|
||||
this.$router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
portalSlug: portal.slug,
|
||||
locale: code,
|
||||
},
|
||||
});
|
||||
this.$emit('fetch-portal');
|
||||
this.$emit('open-portal-page');
|
||||
},
|
||||
isLocaleActive(code, slug) {
|
||||
const isPortalActive = this.portal.slug === slug;
|
||||
const isLocaleActive = this.activeLocale === code;
|
||||
return isPortalActive && isLocaleActive;
|
||||
},
|
||||
isLocaleDefault(code) {
|
||||
return this.portal?.meta?.default_locale === code;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="portal" :class="{ active }">
|
||||
<thumbnail :username="portal.name" variant="square" />
|
||||
@@ -70,77 +141,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
thumbnail,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
activePortalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLocale: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locales() {
|
||||
return this.portal?.config?.allowed_locales;
|
||||
},
|
||||
articlesCount() {
|
||||
const { allowed_locales: allowedLocales } = this.portal.config;
|
||||
return allowedLocales.reduce((acc, locale) => {
|
||||
return acc + locale.articles_count;
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.selectedLocale = this.locale || this.portal?.meta?.default_locale;
|
||||
},
|
||||
methods: {
|
||||
onClick(event, code, portal) {
|
||||
event.preventDefault();
|
||||
this.$router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
portalSlug: portal.slug,
|
||||
locale: code,
|
||||
},
|
||||
});
|
||||
this.$emit('fetch-portal');
|
||||
this.$emit('open-portal-page');
|
||||
},
|
||||
isLocaleActive(code, slug) {
|
||||
const isPortalActive = this.portal.slug === slug;
|
||||
const isLocaleActive = this.activeLocale === code;
|
||||
return isPortalActive && isLocaleActive;
|
||||
},
|
||||
isLocaleDefault(code) {
|
||||
return this.portal?.meta?.default_locale === code;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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;
|
||||
|
||||
+39
-39
@@ -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';
|
||||
@@ -100,3 +61,42 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
+35
-35
@@ -1,3 +1,38 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
thumbnailSrc: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
portalLink: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
popoutHelpCenter() {
|
||||
window.open(this.portalLink, '_blank');
|
||||
},
|
||||
openPortalPopover() {
|
||||
this.$emit('open-popover');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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"
|
||||
@@ -38,38 +73,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
thumbnailSrc: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
portalLink: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
popoutHelpCenter() {
|
||||
window.open(this.portalLink, '_blank');
|
||||
},
|
||||
openPortalPopover() {
|
||||
this.$emit('open-popover');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+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>
|
||||
|
||||
+121
-121
@@ -1,3 +1,124 @@
|
||||
<script>
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { isEmptyObject } from 'dashboard/helper/commons.js';
|
||||
export default {
|
||||
components: {
|
||||
MultiselectDropdown,
|
||||
},
|
||||
props: {
|
||||
article: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
metaTitle: '',
|
||||
metaDescription: '',
|
||||
metaTags: [],
|
||||
metaOptions: [],
|
||||
tagInputValue: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
categories: 'categories/allCategories',
|
||||
agents: 'agents/getAgents',
|
||||
}),
|
||||
assignedAuthor() {
|
||||
return this.article?.author;
|
||||
},
|
||||
selectedCategory() {
|
||||
return this.article?.category;
|
||||
},
|
||||
allTags() {
|
||||
return this.metaTags.map(item => item.name);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
article: {
|
||||
handler() {
|
||||
if (!isEmptyObject(this.article.meta || {})) {
|
||||
const {
|
||||
meta: { title = '', description = '', tags = [] },
|
||||
} = this.article;
|
||||
this.metaTitle = title;
|
||||
this.metaDescription = description;
|
||||
this.metaTags = this.formattedTags({ tags });
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.saveArticle = debounce(
|
||||
() => {
|
||||
this.$emit('save-article', {
|
||||
meta: {
|
||||
title: this.metaTitle,
|
||||
description: this.metaDescription,
|
||||
tags: this.allTags,
|
||||
},
|
||||
});
|
||||
},
|
||||
1000,
|
||||
false
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
formattedTags({ tags }) {
|
||||
return tags.map(tag => ({
|
||||
name: tag,
|
||||
}));
|
||||
},
|
||||
addTagValue(tagValue) {
|
||||
const tags = tagValue
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag && !this.allTags.includes(tag));
|
||||
|
||||
this.metaTags.push(...this.formattedTags({ tags: [...new Set(tags)] }));
|
||||
this.saveArticle();
|
||||
},
|
||||
removeTag() {
|
||||
this.saveArticle();
|
||||
},
|
||||
handleSearchChange(value) {
|
||||
this.tagInputValue = value;
|
||||
},
|
||||
onBlur() {
|
||||
if (this.tagInputValue) {
|
||||
this.addTagValue(this.tagInputValue);
|
||||
}
|
||||
},
|
||||
onClickSelectCategory({ id }) {
|
||||
this.$emit('save-article', { category_id: id });
|
||||
},
|
||||
onClickAssignAuthor({ id }) {
|
||||
this.$emit('save-article', { author_id: id });
|
||||
this.updateMeta();
|
||||
},
|
||||
onChangeMetaInput() {
|
||||
this.saveArticle();
|
||||
},
|
||||
onClickArchiveArticle() {
|
||||
this.$emit('archive-article');
|
||||
this.updateMeta();
|
||||
},
|
||||
onClickDeleteArticle() {
|
||||
this.$emit('delete-article');
|
||||
this.updateMeta();
|
||||
},
|
||||
updateMeta() {
|
||||
this.$emit('update-meta');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="popover-animation">
|
||||
<div
|
||||
@@ -122,127 +243,6 @@
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { isEmptyObject } from 'dashboard/helper/commons.js';
|
||||
export default {
|
||||
components: {
|
||||
MultiselectDropdown,
|
||||
},
|
||||
props: {
|
||||
article: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
metaTitle: '',
|
||||
metaDescription: '',
|
||||
metaTags: [],
|
||||
metaOptions: [],
|
||||
tagInputValue: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
categories: 'categories/allCategories',
|
||||
agents: 'agents/getAgents',
|
||||
}),
|
||||
assignedAuthor() {
|
||||
return this.article?.author;
|
||||
},
|
||||
selectedCategory() {
|
||||
return this.article?.category;
|
||||
},
|
||||
allTags() {
|
||||
return this.metaTags.map(item => item.name);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
article: {
|
||||
handler() {
|
||||
if (!isEmptyObject(this.article.meta || {})) {
|
||||
const {
|
||||
meta: { title = '', description = '', tags = [] },
|
||||
} = this.article;
|
||||
this.metaTitle = title;
|
||||
this.metaDescription = description;
|
||||
this.metaTags = this.formattedTags({ tags });
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.saveArticle = debounce(
|
||||
() => {
|
||||
this.$emit('save-article', {
|
||||
meta: {
|
||||
title: this.metaTitle,
|
||||
description: this.metaDescription,
|
||||
tags: this.allTags,
|
||||
},
|
||||
});
|
||||
},
|
||||
1000,
|
||||
false
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
formattedTags({ tags }) {
|
||||
return tags.map(tag => ({
|
||||
name: tag,
|
||||
}));
|
||||
},
|
||||
addTagValue(tagValue) {
|
||||
const tags = tagValue
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag && !this.allTags.includes(tag));
|
||||
|
||||
this.metaTags.push(...this.formattedTags({ tags: [...new Set(tags)] }));
|
||||
this.saveArticle();
|
||||
},
|
||||
removeTag() {
|
||||
this.saveArticle();
|
||||
},
|
||||
handleSearchChange(value) {
|
||||
this.tagInputValue = value;
|
||||
},
|
||||
onBlur() {
|
||||
if (this.tagInputValue) {
|
||||
this.addTagValue(this.tagInputValue);
|
||||
}
|
||||
},
|
||||
onClickSelectCategory({ id }) {
|
||||
this.$emit('save-article', { category_id: id });
|
||||
},
|
||||
onClickAssignAuthor({ id }) {
|
||||
this.$emit('save-article', { author_id: id });
|
||||
this.updateMeta();
|
||||
},
|
||||
onChangeMetaInput() {
|
||||
this.saveArticle();
|
||||
},
|
||||
onClickArchiveArticle() {
|
||||
this.$emit('archive-article');
|
||||
this.updateMeta();
|
||||
},
|
||||
onClickDeleteArticle() {
|
||||
this.$emit('delete-article');
|
||||
this.updateMeta();
|
||||
},
|
||||
updateMeta() {
|
||||
this.$emit('update-meta');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.multiselect {
|
||||
|
||||
+8
-8
@@ -1,11 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center w-full text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -66,3 +58,11 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center w-full text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+48
-48
@@ -1,51 +1,3 @@
|
||||
<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 }"
|
||||
>
|
||||
<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="h-full p-4 text-base text-center">
|
||||
<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';
|
||||
@@ -215,3 +167,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 }"
|
||||
>
|
||||
<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="h-full p-4 text-base text-center">
|
||||
<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>
|
||||
|
||||
+39
-39
@@ -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';
|
||||
@@ -193,3 +154,42 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
+25
-25
@@ -1,28 +1,3 @@
|
||||
<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 }"
|
||||
>
|
||||
<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';
|
||||
@@ -117,3 +92,28 @@ export default {
|
||||
},
|
||||
};
|
||||
</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 }"
|
||||
>
|
||||
<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>
|
||||
|
||||
+68
-68
@@ -1,71 +1,3 @@
|
||||
<!-- 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>
|
||||
<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 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>
|
||||
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -178,6 +110,74 @@ export default {
|
||||
},
|
||||
};
|
||||
</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>
|
||||
<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 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;
|
||||
|
||||
+20
-20
@@ -1,3 +1,23 @@
|
||||
<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>
|
||||
@@ -73,26 +93,6 @@
|
||||
</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 {
|
||||
|
||||
+70
-70
@@ -1,73 +1,3 @@
|
||||
<!-- 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>
|
||||
<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 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>
|
||||
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -189,6 +119,76 @@ export default {
|
||||
},
|
||||
};
|
||||
</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>
|
||||
<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 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%;
|
||||
|
||||
+37
-37
@@ -1,40 +1,3 @@
|
||||
<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.vue');
|
||||
|
||||
@@ -113,6 +76,43 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.emoji-dialog::before {
|
||||
@apply hidden;
|
||||
|
||||
@@ -1,36 +1,3 @@
|
||||
<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';
|
||||
@@ -99,6 +66,39 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<style scoped lang="scss">
|
||||
.wrapper {
|
||||
flex: 1;
|
||||
|
||||
+53
-53
@@ -1,3 +1,56 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import PortalListItem from '../../components/PortalListItem.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import AddLocale from '../../components/AddLocale.vue';
|
||||
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PortalListItem,
|
||||
EmptyState,
|
||||
Spinner,
|
||||
AddLocale,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isAddLocaleModalOpen: false,
|
||||
selectedPortal: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
portals: 'portals/allPortals',
|
||||
meta: 'portals/getMeta',
|
||||
isFetching: 'portals/isFetchingPortals',
|
||||
}),
|
||||
portalStatus() {
|
||||
return this.archived ? 'Archived' : 'Live';
|
||||
},
|
||||
shouldShowEmptyState() {
|
||||
return !this.isFetching && !this.portals.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openPortal(portalSlug) {
|
||||
window.open(buildPortalURL(portalSlug), '_blank');
|
||||
},
|
||||
addPortal() {
|
||||
this.$router.push({ name: 'new_portal_information' });
|
||||
},
|
||||
closeAddLocaleModal() {
|
||||
this.isAddLocaleModalOpen = false;
|
||||
this.selectedPortal = {};
|
||||
},
|
||||
addLocale(portalId) {
|
||||
this.isAddLocaleModalOpen = true;
|
||||
this.selectedPortal = this.portals.find(portal => portal.id === portalId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</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">
|
||||
@@ -51,56 +104,3 @@
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import PortalListItem from '../../components/PortalListItem.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import AddLocale from '../../components/AddLocale.vue';
|
||||
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PortalListItem,
|
||||
EmptyState,
|
||||
Spinner,
|
||||
AddLocale,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isAddLocaleModalOpen: false,
|
||||
selectedPortal: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
portals: 'portals/allPortals',
|
||||
meta: 'portals/getMeta',
|
||||
isFetching: 'portals/isFetchingPortals',
|
||||
}),
|
||||
portalStatus() {
|
||||
return this.archived ? 'Archived' : 'Live';
|
||||
},
|
||||
shouldShowEmptyState() {
|
||||
return !this.isFetching && !this.portals.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openPortal(portalSlug) {
|
||||
window.open(buildPortalURL(portalSlug), '_blank');
|
||||
},
|
||||
addPortal() {
|
||||
this.$router.push({ name: 'new_portal_information' });
|
||||
},
|
||||
closeAddLocaleModal() {
|
||||
this.isAddLocaleModalOpen = false;
|
||||
this.selectedPortal = {};
|
||||
},
|
||||
addLocale(portalId) {
|
||||
this.isAddLocaleModalOpen = true;
|
||||
this.selectedPortal = this.portals.find(portal => portal.id === portalId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -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">
|
||||
<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>
|
||||
|
||||
+12
-12
@@ -1,15 +1,3 @@
|
||||
<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 { useAlert } from 'dashboard/composables';
|
||||
@@ -68,3 +56,15 @@ export default {
|
||||
},
|
||||
};
|
||||
</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>
|
||||
|
||||
+8
-8
@@ -1,3 +1,11 @@
|
||||
<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"
|
||||
@@ -21,11 +29,3 @@
|
||||
</empty-state>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
defineComponent({
|
||||
name: 'PortalSettingsFinish',
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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,49 +1,3 @@
|
||||
<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="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>
|
||||
<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';
|
||||
@@ -212,3 +166,49 @@ 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'"
|
||||
>
|
||||
<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="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>
|
||||
<intersection-observer
|
||||
v-if="!showEndOfList && !uiFlags.isFetching"
|
||||
:options="infiniteLoaderOptions"
|
||||
@observed="loadMoreNotifications"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<router-view />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,38 +1,3 @@
|
||||
<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 w-full h-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="my-4 spinner" />
|
||||
</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';
|
||||
@@ -207,3 +172,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">
|
||||
<inbox-empty-state
|
||||
:empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="flex flex-col w-full h-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="my-4 spinner" />
|
||||
</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>
|
||||
|
||||
@@ -1,62 +1,3 @@
|
||||
<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>
|
||||
<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';
|
||||
@@ -215,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>
|
||||
<inbox-context-menu
|
||||
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"
|
||||
>
|
||||
<menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
@click="onMenuItemClick(item.key)"
|
||||
/>
|
||||
</div>
|
||||
</woot-context-menu>
|
||||
</template>
|
||||
|
||||
@@ -1,101 +1,3 @@
|
||||
<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>
|
||||
|
||||
<script>
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -203,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>
|
||||
|
||||
@@ -1,59 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-2 py-2 border-b ltr:pl-4 rtl:pl-2 h-14 ltr:pr-2 rtl:pr-4 rtl:border-r border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<woot-button
|
||||
variant="clear link"
|
||||
class="flex md:hidden !pt-1 !pb-1 rounded-md ltr:pr-1 rtl:pl-1 !no-underline"
|
||||
size="medium"
|
||||
color-scheme="primary"
|
||||
icon="chevron-left"
|
||||
@click="onClickGoToInboxList"
|
||||
>
|
||||
{{ $t('INBOX.ACTION_HEADER.BACK') }}
|
||||
</woot-button>
|
||||
<pagination-button
|
||||
v-if="totalLength > 1"
|
||||
:total-length="totalLength"
|
||||
:current-index="currentIndex + 1"
|
||||
@next="onClickNext"
|
||||
@prev="onClickPrev"
|
||||
/>
|
||||
<div v-else />
|
||||
<div class="flex items-center gap-2">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="snooze"
|
||||
class="[&>span]:hidden md:[&>span]:inline-flex"
|
||||
@click="openSnoozeNotificationModal"
|
||||
>
|
||||
{{ $t('INBOX.ACTION_HEADER.SNOOZE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
icon="delete"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
class="[&>span]:hidden md:[&>span]:inline-flex"
|
||||
@click="deleteNotification"
|
||||
>
|
||||
{{ $t('INBOX.ACTION_HEADER.DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="scheduleCustomSnooze"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -156,3 +100,59 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between w-full gap-2 py-2 border-b ltr:pl-4 rtl:pl-2 h-14 ltr:pr-2 rtl:pr-4 rtl:border-r border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<woot-button
|
||||
variant="clear link"
|
||||
class="flex md:hidden !pt-1 !pb-1 rounded-md ltr:pr-1 rtl:pl-1 !no-underline"
|
||||
size="medium"
|
||||
color-scheme="primary"
|
||||
icon="chevron-left"
|
||||
@click="onClickGoToInboxList"
|
||||
>
|
||||
{{ $t('INBOX.ACTION_HEADER.BACK') }}
|
||||
</woot-button>
|
||||
<pagination-button
|
||||
v-if="totalLength > 1"
|
||||
:total-length="totalLength"
|
||||
:current-index="currentIndex + 1"
|
||||
@next="onClickNext"
|
||||
@prev="onClickPrev"
|
||||
/>
|
||||
<div v-else />
|
||||
<div class="flex items-center gap-2">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="snooze"
|
||||
class="[&>span]:hidden md:[&>span]:inline-flex"
|
||||
@click="openSnoozeNotificationModal"
|
||||
>
|
||||
{{ $t('INBOX.ACTION_HEADER.SNOOZE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
icon="delete"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
class="[&>span]:hidden md:[&>span]:inline-flex"
|
||||
@click="deleteNotification"
|
||||
>
|
||||
{{ $t('INBOX.ACTION_HEADER.DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="scheduleCustomSnooze"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,61 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between w-full py-2 border-b ltr:pl-4 rtl:pl-2 rtl:pr-4 ltr:pr-2 h-14 border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<h1 class="text-xl font-medium text-slate-900 dark:text-slate-25">
|
||||
{{ $t('INBOX.LIST.TITLE') }}
|
||||
</h1>
|
||||
<div class="relative">
|
||||
<div
|
||||
role="button"
|
||||
class="flex items-center gap-1 px-2 py-1 border rounded-md border-slate-100 dark:border-slate-700/50"
|
||||
@click="openInboxDisplayMenu"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium text-center text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('INBOX.LIST.DISPLAY_DROPDOWN') }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
icon="chevron-down"
|
||||
size="12"
|
||||
class="text-slate-600 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<inbox-display-menu
|
||||
v-if="showInboxDisplayMenu"
|
||||
v-on-clickaway="openInboxDisplayMenu"
|
||||
class="absolute top-9 ltr:left-0 rtl:right-0"
|
||||
@filter="onFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex items-center gap-1">
|
||||
<!-- <woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="filter"
|
||||
@click="openInboxFilter"
|
||||
/> -->
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="mail-inbox"
|
||||
@click="openInboxOptionsMenu"
|
||||
/>
|
||||
<inbox-option-menu
|
||||
v-if="showInboxOptionMenu"
|
||||
v-on-clickaway="openInboxOptionsMenu"
|
||||
class="absolute top-9 ltr:right-0 ltr:md:right-[unset] rtl:left-0 rtl:md:left-[unset]"
|
||||
@option-click="onInboxOptionMenuClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
@@ -132,4 +74,62 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between w-full py-2 border-b ltr:pl-4 rtl:pl-2 rtl:pr-4 ltr:pr-2 h-14 border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<h1 class="text-xl font-medium text-slate-900 dark:text-slate-25">
|
||||
{{ $t('INBOX.LIST.TITLE') }}
|
||||
</h1>
|
||||
<div class="relative">
|
||||
<div
|
||||
role="button"
|
||||
class="flex items-center gap-1 px-2 py-1 border rounded-md border-slate-100 dark:border-slate-700/50"
|
||||
@click="openInboxDisplayMenu"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium text-center text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('INBOX.LIST.DISPLAY_DROPDOWN') }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
icon="chevron-down"
|
||||
size="12"
|
||||
class="text-slate-600 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
<inbox-display-menu
|
||||
v-if="showInboxDisplayMenu"
|
||||
v-on-clickaway="openInboxDisplayMenu"
|
||||
class="absolute top-9 ltr:left-0 rtl:right-0"
|
||||
@filter="onFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex items-center gap-1">
|
||||
<!-- <woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="filter"
|
||||
@click="openInboxFilter"
|
||||
/> -->
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="mail-inbox"
|
||||
@click="openInboxOptionsMenu"
|
||||
/>
|
||||
<inbox-option-menu
|
||||
v-if="showInboxOptionMenu"
|
||||
v-on-clickaway="openInboxOptionsMenu"
|
||||
class="absolute top-9 ltr:right-0 ltr:md:right-[unset] rtl:left-0 rtl:md:left-[unset]"
|
||||
@option-click="onInboxOptionMenuClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-1 bg-white z-50 dark:bg-slate-900 w-40 py-1 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 flex-col">
|
||||
<menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
@click="onClick(item.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuItem from './MenuItem.vue';
|
||||
export default {
|
||||
@@ -44,3 +29,18 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-1 bg-white z-50 dark:bg-slate-900 w-40 py-1 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 flex-col">
|
||||
<menu-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
:label="item.label"
|
||||
@click="onClick(item.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
totalLength: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isUpDisabled() {
|
||||
return this.currentIndex === 1;
|
||||
},
|
||||
isDownDisabled() {
|
||||
return this.currentIndex === this.totalLength || this.totalLength <= 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleUpClick() {
|
||||
if (this.currentIndex > 1) {
|
||||
this.$emit('prev');
|
||||
}
|
||||
},
|
||||
handleDownClick() {
|
||||
if (this.currentIndex < this.totalLength) {
|
||||
this.$emit('next');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex gap-1 items-center">
|
||||
@@ -34,37 +68,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
totalLength: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isUpDisabled() {
|
||||
return this.currentIndex === 1;
|
||||
},
|
||||
isDownDisabled() {
|
||||
return this.currentIndex === this.totalLength || this.totalLength <= 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleUpClick() {
|
||||
if (this.currentIndex > 1) {
|
||||
this.$emit('prev');
|
||||
}
|
||||
},
|
||||
handleDownClick() {
|
||||
if (this.currentIndex < this.totalLength) {
|
||||
this.$emit('next');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
<script>
|
||||
import { CONVERSATION_STATUS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONVERSATION_STATUS,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="inline-flex items-center justify-center rounded-md">
|
||||
<!-- Pending -->
|
||||
@@ -66,21 +84,3 @@
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CONVERSATION_STATUS } from 'shared/constants/messages';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONVERSATION_STATUS,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+115
-115
@@ -1,118 +1,3 @@
|
||||
<template>
|
||||
<div class="modal-mask">
|
||||
<div
|
||||
v-on-clickaway="closeNotificationPanel"
|
||||
class="flex-col h-[90vh] w-[32.5rem] flex justify-between z-10 rounded-md shadow-md absolute bg-white dark:bg-slate-800 left-14 rtl:left-auto rtl:right-14 m-4"
|
||||
>
|
||||
<div
|
||||
class="flex-row items-center border-b border-solid pt-5 pb-3 px-6 border-slate-50 dark:border-slate-700 w-full flex justify-between"
|
||||
>
|
||||
<div class="items-center flex">
|
||||
<span class="text-xl font-bold text-slate-800 dark:text-slate-100">
|
||||
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.TITLE') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="totalUnreadNotifications"
|
||||
class="py-1 px-2 font-semibold text-slate-700 dark:text-slate-200 rounded-md text-xxs ml-2 mr-2 bg-slate-50 dark:bg-slate-700"
|
||||
>
|
||||
{{ totalUnreadNotifications }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<woot-button
|
||||
v-if="!noUnreadNotificationAvailable"
|
||||
color-scheme="primary"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
@click="onMarkAllDoneClick"
|
||||
>
|
||||
{{ $t('NOTIFICATIONS_PAGE.MARK_ALL_DONE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="settings"
|
||||
@click="openAudioNotificationSettings"
|
||||
/>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
size="tiny"
|
||||
icon="dismiss"
|
||||
@click="closeNotificationPanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<notification-panel-list
|
||||
:notifications="getUnreadNotifications"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:on-click-notification="openConversation"
|
||||
:in-last-page="inLastPage"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<div
|
||||
v-if="records.length !== 0"
|
||||
class="items-center py-1 px-5 flex justify-between"
|
||||
>
|
||||
<div class="flex">
|
||||
<woot-button
|
||||
size="medium"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
:is-disabled="inFirstPage"
|
||||
@click="onClickFirstPage"
|
||||
>
|
||||
<fluent-icon icon="chevron-left" size="16" />
|
||||
<fluent-icon
|
||||
icon="chevron-left"
|
||||
size="16"
|
||||
:class="notificationPanelFooterIconClass"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="clear"
|
||||
size="medium"
|
||||
icon="chevron-left"
|
||||
:disabled="inFirstPage"
|
||||
@click="onClickPreviousPage"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-xxs font-semibold text-slate-500 dark:text-slate-400">
|
||||
{{ currentPage }} - {{ lastPage }}
|
||||
</span>
|
||||
<div class="flex">
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="clear"
|
||||
size="medium"
|
||||
icon="chevron-right"
|
||||
:disabled="inLastPage"
|
||||
@click="onClickNextPage"
|
||||
/>
|
||||
<woot-button
|
||||
size="medium"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
:disabled="inLastPage"
|
||||
@click="onClickLastPage"
|
||||
>
|
||||
<fluent-icon icon="chevron-right" size="16" />
|
||||
<fluent-icon
|
||||
icon="chevron-right"
|
||||
size="16"
|
||||
:class="notificationPanelFooterIconClass"
|
||||
/>
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
@@ -246,3 +131,118 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal-mask">
|
||||
<div
|
||||
v-on-clickaway="closeNotificationPanel"
|
||||
class="flex-col h-[90vh] w-[32.5rem] flex justify-between z-10 rounded-md shadow-md absolute bg-white dark:bg-slate-800 left-14 rtl:left-auto rtl:right-14 m-4"
|
||||
>
|
||||
<div
|
||||
class="flex-row items-center border-b border-solid pt-5 pb-3 px-6 border-slate-50 dark:border-slate-700 w-full flex justify-between"
|
||||
>
|
||||
<div class="items-center flex">
|
||||
<span class="text-xl font-bold text-slate-800 dark:text-slate-100">
|
||||
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.TITLE') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="totalUnreadNotifications"
|
||||
class="py-1 px-2 font-semibold text-slate-700 dark:text-slate-200 rounded-md text-xxs ml-2 mr-2 bg-slate-50 dark:bg-slate-700"
|
||||
>
|
||||
{{ totalUnreadNotifications }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<woot-button
|
||||
v-if="!noUnreadNotificationAvailable"
|
||||
color-scheme="primary"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
@click="onMarkAllDoneClick"
|
||||
>
|
||||
{{ $t('NOTIFICATIONS_PAGE.MARK_ALL_DONE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
icon="settings"
|
||||
@click="openAudioNotificationSettings"
|
||||
/>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
size="tiny"
|
||||
icon="dismiss"
|
||||
@click="closeNotificationPanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<notification-panel-list
|
||||
:notifications="getUnreadNotifications"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:on-click-notification="openConversation"
|
||||
:in-last-page="inLastPage"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<div
|
||||
v-if="records.length !== 0"
|
||||
class="items-center py-1 px-5 flex justify-between"
|
||||
>
|
||||
<div class="flex">
|
||||
<woot-button
|
||||
size="medium"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
:is-disabled="inFirstPage"
|
||||
@click="onClickFirstPage"
|
||||
>
|
||||
<fluent-icon icon="chevron-left" size="16" />
|
||||
<fluent-icon
|
||||
icon="chevron-left"
|
||||
size="16"
|
||||
:class="notificationPanelFooterIconClass"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="clear"
|
||||
size="medium"
|
||||
icon="chevron-left"
|
||||
:disabled="inFirstPage"
|
||||
@click="onClickPreviousPage"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-xxs font-semibold text-slate-500 dark:text-slate-400">
|
||||
{{ currentPage }} - {{ lastPage }}
|
||||
</span>
|
||||
<div class="flex">
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
variant="clear"
|
||||
size="medium"
|
||||
icon="chevron-right"
|
||||
:disabled="inLastPage"
|
||||
@click="onClickNextPage"
|
||||
/>
|
||||
<woot-button
|
||||
size="medium"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
:disabled="inLastPage"
|
||||
@click="onClickLastPage"
|
||||
>
|
||||
<fluent-icon icon="chevron-right" size="16" />
|
||||
<fluent-icon
|
||||
icon="chevron-right"
|
||||
size="16"
|
||||
:class="notificationPanelFooterIconClass"
|
||||
/>
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+38
-38
@@ -1,3 +1,41 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
notificationAssignee() {
|
||||
const { primary_actor: primaryActor } = this.notificationItem;
|
||||
return primaryActor?.meta?.assignee;
|
||||
},
|
||||
hasNotificationAssignee() {
|
||||
return !!this.notificationAssignee;
|
||||
},
|
||||
notificationAssigneeName() {
|
||||
return this.notificationAssignee?.name || '';
|
||||
},
|
||||
notificationAssigneeThumbnail() {
|
||||
return this.notificationAssignee?.thumbnail || '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
onClickOpenNotification() {
|
||||
this.$emit('open-notification', this.notificationItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<woot-button
|
||||
@@ -64,41 +102,3 @@
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
notificationAssignee() {
|
||||
const { primary_actor: primaryActor } = this.notificationItem;
|
||||
return primaryActor?.meta?.assignee;
|
||||
},
|
||||
hasNotificationAssignee() {
|
||||
return !!this.notificationAssignee;
|
||||
},
|
||||
notificationAssigneeName() {
|
||||
return this.notificationAssignee?.name || '';
|
||||
},
|
||||
notificationAssigneeThumbnail() {
|
||||
return this.notificationAssignee?.thumbnail || '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
onClickOpenNotification() {
|
||||
this.$emit('open-notification', this.notificationItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+35
-35
@@ -1,38 +1,3 @@
|
||||
<template>
|
||||
<div class="flex-col py-2 px-2.5 overflow-auto h-full flex">
|
||||
<notification-panel-item
|
||||
v-for="notificationItem in notifications"
|
||||
v-show="!isLoading"
|
||||
:key="notificationItem.id"
|
||||
:notification-item="notificationItem"
|
||||
@open-notification="onClickNotification"
|
||||
/>
|
||||
<empty-state
|
||||
v-if="showEmptyResult"
|
||||
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="!isLoading && inLastPage"
|
||||
size="expanded"
|
||||
variant="clear"
|
||||
color-scheme="primary"
|
||||
class-names="mt-3"
|
||||
@click="openNotificationPage"
|
||||
>
|
||||
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.ALL_NOTIFICATIONS') }}
|
||||
</woot-button>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="items-center justify-center my-12 mx-2 text-sm font-medium flex"
|
||||
>
|
||||
<spinner />
|
||||
<span>{{
|
||||
$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.LOADING_UNREAD_MESSAGE')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
@@ -84,3 +49,38 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col py-2 px-2.5 overflow-auto h-full flex">
|
||||
<notification-panel-item
|
||||
v-for="notificationItem in notifications"
|
||||
v-show="!isLoading"
|
||||
:key="notificationItem.id"
|
||||
:notification-item="notificationItem"
|
||||
@open-notification="onClickNotification"
|
||||
/>
|
||||
<empty-state
|
||||
v-if="showEmptyResult"
|
||||
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="!isLoading && inLastPage"
|
||||
size="expanded"
|
||||
variant="clear"
|
||||
color-scheme="primary"
|
||||
class-names="mt-3"
|
||||
@click="openNotificationPage"
|
||||
>
|
||||
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.ALL_NOTIFICATIONS') }}
|
||||
</woot-button>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="items-center justify-center my-12 mx-2 text-sm font-medium flex"
|
||||
>
|
||||
<spinner />
|
||||
<span>{{
|
||||
$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.LOADING_UNREAD_MESSAGE')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+49
-49
@@ -1,3 +1,52 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
},
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isUpdating: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onClickNotification: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onMarkAllDoneClick: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
notificationMetadata: 'notifications/getMeta',
|
||||
}),
|
||||
showEmptyResult() {
|
||||
return !this.isLoading && this.notifications.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex-grow flex-shrink h-full px-4 py-8 overflow-hidden bg-white dark:bg-slate-900"
|
||||
@@ -84,55 +133,6 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
},
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isUpdating: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onClickNotification: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onMarkAllDoneClick: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
notificationMetadata: 'notifications/getMeta',
|
||||
}),
|
||||
showEmptyResult() {
|
||||
return !this.isLoading && this.notifications.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
|
||||
+21
-21
@@ -1,24 +1,3 @@
|
||||
<template>
|
||||
<div class="overflow-y-auto h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<notification-table
|
||||
:notifications="records"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:is-updating="uiFlags.isUpdating"
|
||||
:on-click-notification="openConversation"
|
||||
:on-mark-all-done-click="onMarkAllDoneClick"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||
@@ -77,6 +56,27 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-y-auto h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<notification-table
|
||||
:notifications="records"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:is-updating="uiFlags.isUpdating"
|
||||
:on-click-notification="openConversation"
|
||||
:on-mark-all-done-click="onMarkAllDoneClick"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notification--page {
|
||||
background: var(--white);
|
||||
|
||||
@@ -1,39 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between items-center h-14 min-h-[3.5rem] px-4 py-2 bg-white dark:bg-slate-900 border-b border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<h1
|
||||
class="flex items-center mb-0 text-2xl text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
<woot-sidemenu-icon v-if="showSidemenuIcon" />
|
||||
<back-button
|
||||
v-if="showBackButton"
|
||||
:button-label="backButtonLabel"
|
||||
:back-url="backUrl"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-if="icon"
|
||||
:icon="icon"
|
||||
:class="iconClass"
|
||||
class="hidden ml-1 mr-2 rtl:ml-2 rtl:mr-1 md:block"
|
||||
/>
|
||||
<slot />
|
||||
<span class="text-2xl font-medium text-slate-900 dark:text-slate-100">
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
</h1>
|
||||
<router-link
|
||||
v-if="showNewButton && isAdmin"
|
||||
:to="buttonRoute"
|
||||
class="button success button--fixed-top px-3.5 py-1 rounded-[5px] flex gap-2"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
<span class="button__content">
|
||||
{{ buttonText }}
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
@@ -91,3 +55,39 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between items-center h-14 min-h-[3.5rem] px-4 py-2 bg-white dark:bg-slate-900 border-b border-slate-50 dark:border-slate-800/50"
|
||||
>
|
||||
<h1
|
||||
class="flex items-center mb-0 text-2xl text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
<woot-sidemenu-icon v-if="showSidemenuIcon" />
|
||||
<back-button
|
||||
v-if="showBackButton"
|
||||
:button-label="backButtonLabel"
|
||||
:back-url="backUrl"
|
||||
/>
|
||||
<fluent-icon
|
||||
v-if="icon"
|
||||
:icon="icon"
|
||||
:class="iconClass"
|
||||
class="hidden ml-1 mr-2 rtl:ml-2 rtl:mr-1 md:block"
|
||||
/>
|
||||
<slot />
|
||||
<span class="text-2xl font-medium text-slate-900 dark:text-slate-100">
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
</h1>
|
||||
<router-link
|
||||
v-if="showNewButton && isAdmin"
|
||||
:to="buttonRoute"
|
||||
class="button success button--fixed-top px-3.5 py-1 rounded-[5px] flex gap-2"
|
||||
>
|
||||
<fluent-icon icon="add-circle" />
|
||||
<span class="button__content">
|
||||
{{ buttonText }}
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: { type: String, default: '' },
|
||||
headerContent: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full items-start">
|
||||
<h2
|
||||
@@ -11,12 +20,3 @@
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: { type: String, default: '' },
|
||||
headerContent: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-1 h-full justify-between flex-col m-0 bg-slate-25 dark:bg-slate-900 overflow-auto"
|
||||
>
|
||||
<settings-header
|
||||
button-route="new"
|
||||
:icon="icon"
|
||||
:header-title="$t(headerTitle)"
|
||||
:button-text="$t(headerButtonText)"
|
||||
:show-back-button="showBackButton"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="showNewButton"
|
||||
:show-sidemenu-icon="showSidemenuIcon"
|
||||
/>
|
||||
<keep-alive v-if="keepAlive">
|
||||
<router-view />
|
||||
</keep-alive>
|
||||
<router-view v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
import SettingsHeader from './SettingsHeader.vue';
|
||||
@@ -65,3 +44,24 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-1 h-full justify-between flex-col m-0 bg-slate-25 dark:bg-slate-900 overflow-auto"
|
||||
>
|
||||
<settings-header
|
||||
button-route="new"
|
||||
:icon="icon"
|
||||
:header-title="$t(headerTitle)"
|
||||
:button-text="$t(headerButtonText)"
|
||||
:show-back-button="showBackButton"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="showNewButton"
|
||||
:show-sidemenu-icon="showSidemenuIcon"
|
||||
/>
|
||||
<keep-alive v-if="keepAlive">
|
||||
<router-view />
|
||||
</keep-alive>
|
||||
<router-view v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,133 +1,3 @@
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink min-w-0 p-6 overflow-auto">
|
||||
<form v-if="!uiFlags.isFetchingItem" @submit.prevent="updateAccount">
|
||||
<div
|
||||
class="flex flex-row p-4 border-b border-slate-25 dark:border-slate-800"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
<span v-if="v$.name.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.locale.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.LABEL') }}
|
||||
<select v-model="locale">
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.locale.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED') }}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED')
|
||||
}}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL') }}
|
||||
<input
|
||||
v-model="domain"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="featureCustomReplyEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model="supportEmail"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="showAutoResolutionConfig"
|
||||
:class="{ error: v$.autoResolveDuration.$error }"
|
||||
>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.LABEL') }}
|
||||
<input
|
||||
v-model="autoResolveDuration"
|
||||
type="number"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.autoResolveDuration.$touch"
|
||||
/>
|
||||
<span v-if="v$.autoResolveDuration.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-row p-4 border-slate-25 dark:border-slate-700 text-black-900 dark:text-slate-300"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<woot-code :script="getAccountId" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 text-sm text-center">
|
||||
<div>{{ `v${globalConfig.appVersion}` }}</div>
|
||||
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.UPDATE_CHATWOOT', {
|
||||
latestChatwootVersion: latestChatwootVersion,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="build-id">
|
||||
<div>{{ `Build ${globalConfig.gitSha}` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
class="button nice success button--fixed-top"
|
||||
:button-text="$t('GENERAL_SETTINGS.SUBMIT')"
|
||||
:loading="isUpdating"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minValue, maxValue } from '@vuelidate/validators';
|
||||
@@ -289,3 +159,133 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-grow flex-shrink min-w-0 p-6 overflow-auto">
|
||||
<form v-if="!uiFlags.isFetchingItem" @submit.prevent="updateAccount">
|
||||
<div
|
||||
class="flex flex-row p-4 border-b border-slate-25 dark:border-slate-800"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
<span v-if="v$.name.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.locale.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.LABEL') }}
|
||||
<select v-model="locale">
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.locale.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED') }}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED')
|
||||
}}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL') }}
|
||||
<input
|
||||
v-model="domain"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="featureCustomReplyEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model="supportEmail"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="showAutoResolutionConfig"
|
||||
:class="{ error: v$.autoResolveDuration.$error }"
|
||||
>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.LABEL') }}
|
||||
<input
|
||||
v-model="autoResolveDuration"
|
||||
type="number"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.autoResolveDuration.$touch"
|
||||
/>
|
||||
<span v-if="v$.autoResolveDuration.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-row p-4 border-slate-25 dark:border-slate-700 text-black-900 dark:text-slate-300"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<woot-code :script="getAccountId" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 text-sm text-center">
|
||||
<div>{{ `v${globalConfig.appVersion}` }}</div>
|
||||
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.UPDATE_CHATWOOT', {
|
||||
latestChatwootVersion: latestChatwootVersion,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="build-id">
|
||||
<div>{{ `Build ${globalConfig.gitSha}` }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
class="button nice success button--fixed-top"
|
||||
:button-text="$t('GENERAL_SETTINGS.SUBMIT')"
|
||||
:loading="isUpdating"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,49 +1,3 @@
|
||||
<template>
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('AGENT_BOTS.LIST.LOADING')"
|
||||
/>
|
||||
<table v-else-if="agentBots.length" class="woot-table">
|
||||
<tbody>
|
||||
<agent-bot-row
|
||||
v-for="(agentBot, index) in agentBots"
|
||||
:key="agentBot.id"
|
||||
:agent-bot="agentBot"
|
||||
:index="index"
|
||||
@delete="onDeleteAgentBot"
|
||||
@edit="onEditAgentBot"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else class="flex flex-col items-center justify-center h-full">
|
||||
{{ $t('AGENT_BOTS.LIST.404') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="hidden w-1/3 lg:block">
|
||||
<p v-html="$t('AGENT_BOTS.SIDEBAR_TXT')" />
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
icon="add-circle"
|
||||
>
|
||||
<router-link :to="newAgentBotsURL" class="white-text">
|
||||
{{ $t('AGENT_BOTS.ADD.TITLE') }}
|
||||
</router-link>
|
||||
</woot-button>
|
||||
<woot-confirm-modal
|
||||
ref="confirmDialog"
|
||||
:title="$t('AGENT_BOTS.DELETE.TITLE')"
|
||||
:description="$t('AGENT_BOTS.DELETE.DESCRIPTION')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -89,6 +43,52 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 p-4 overflow-auto">
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="w-full lg:w-3/5">
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('AGENT_BOTS.LIST.LOADING')"
|
||||
/>
|
||||
<table v-else-if="agentBots.length" class="woot-table">
|
||||
<tbody>
|
||||
<agent-bot-row
|
||||
v-for="(agentBot, index) in agentBots"
|
||||
:key="agentBot.id"
|
||||
:agent-bot="agentBot"
|
||||
:index="index"
|
||||
@delete="onDeleteAgentBot"
|
||||
@edit="onEditAgentBot"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else class="flex flex-col items-center justify-center h-full">
|
||||
{{ $t('AGENT_BOTS.LIST.404') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="hidden w-1/3 lg:block">
|
||||
<p v-html="$t('AGENT_BOTS.SIDEBAR_TXT')" />
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
color-scheme="success"
|
||||
class-names="button--fixed-top"
|
||||
icon="add-circle"
|
||||
>
|
||||
<router-link :to="newAgentBotsURL" class="white-text">
|
||||
{{ $t('AGENT_BOTS.ADD.TITLE') }}
|
||||
</router-link>
|
||||
</woot-button>
|
||||
<woot-confirm-modal
|
||||
ref="confirmDialog"
|
||||
:title="$t('AGENT_BOTS.DELETE.TITLE')"
|
||||
:description="$t('AGENT_BOTS.DELETE.DESCRIPTION')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.bots-list {
|
||||
list-style: none;
|
||||
|
||||
+24
-24
@@ -1,3 +1,27 @@
|
||||
<script>
|
||||
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
|
||||
import AgentBotType from './AgentBotType.vue';
|
||||
|
||||
export default {
|
||||
components: { ShowMore, AgentBotType },
|
||||
props: {
|
||||
agentBot: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isACSMLTypeBot() {
|
||||
const { bot_type: botType } = this.agentBot;
|
||||
return botType === 'csml';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<tr class="space-x-2">
|
||||
<td class="agent-bot--details">
|
||||
@@ -30,30 +54,6 @@
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<script>
|
||||
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
|
||||
import AgentBotType from './AgentBotType.vue';
|
||||
|
||||
export default {
|
||||
components: { ShowMore, AgentBotType },
|
||||
props: {
|
||||
agentBot: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isACSMLTypeBot() {
|
||||
const { bot_type: botType } = this.agentBot;
|
||||
return botType === 'csml';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.agent-bot--link {
|
||||
align-items: center;
|
||||
|
||||
+11
-11
@@ -1,14 +1,3 @@
|
||||
<template>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<img
|
||||
v-tooltip="botTypeConfig[botType].label"
|
||||
class="agent-bot-type--thumbnail"
|
||||
:src="botTypeConfig[botType].thumbnail"
|
||||
:alt="botTypeConfig[botType].label"
|
||||
/>
|
||||
<span>{{ botTypeConfig[botType].label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
@@ -33,6 +22,17 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<img
|
||||
v-tooltip="botTypeConfig[botType].label"
|
||||
class="agent-bot-type--thumbnail"
|
||||
:src="botTypeConfig[botType].thumbnail"
|
||||
:alt="botTypeConfig[botType].label"
|
||||
/>
|
||||
<span>{{ botTypeConfig[botType].label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<style scoped>
|
||||
.agent-bot-type--thumbnail {
|
||||
width: auto;
|
||||
|
||||
+46
-46
@@ -1,3 +1,49 @@
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import CsmlMonacoEditor from './CSMLMonacoEditor.vue';
|
||||
|
||||
export default {
|
||||
components: { CsmlMonacoEditor },
|
||||
props: {
|
||||
agentBot: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
validations: {
|
||||
bot: {
|
||||
name: { required },
|
||||
csmlContent: { required },
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bot: {
|
||||
name: this.agentBot.name || '',
|
||||
description: this.agentBot.description || '',
|
||||
csmlContent: this.agentBot.bot_config.csml_content || '',
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
this.$emit('submit', {
|
||||
id: this.agentBot.id || '',
|
||||
...this.bot,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<div class="flex flex-row">
|
||||
@@ -48,49 +94,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import CsmlMonacoEditor from './CSMLMonacoEditor.vue';
|
||||
|
||||
export default {
|
||||
components: { CsmlMonacoEditor },
|
||||
props: {
|
||||
agentBot: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
validations: {
|
||||
bot: {
|
||||
name: { required },
|
||||
csmlContent: { required },
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
bot: {
|
||||
name: this.agentBot.name || '',
|
||||
description: this.agentBot.description || '',
|
||||
csmlContent: this.agentBot.bot_config.csml_content || '',
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
this.$emit('submit', {
|
||||
id: this.agentBot.id || '',
|
||||
...this.bot,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
+14
-14
@@ -1,17 +1,3 @@
|
||||
<template>
|
||||
<div class="csml-editor--container">
|
||||
<loading-state
|
||||
v-if="iframeLoading"
|
||||
:message="$t('AGENT_BOTS.LOADING_EDITOR')"
|
||||
class="dashboard-app_loading-container"
|
||||
/>
|
||||
<iframe
|
||||
id="csml-editor--frame"
|
||||
:src="globalConfig.csmlEditorHost"
|
||||
@load="onEditorLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -59,6 +45,20 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="csml-editor--container">
|
||||
<loading-state
|
||||
v-if="iframeLoading"
|
||||
:message="$t('AGENT_BOTS.LOADING_EDITOR')"
|
||||
class="dashboard-app_loading-container"
|
||||
/>
|
||||
<iframe
|
||||
id="csml-editor--frame"
|
||||
:src="globalConfig.csmlEditorHost"
|
||||
@load="onEditorLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#csml-editor--frame {
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
<template>
|
||||
<csml-bot-editor
|
||||
v-if="agentBot.id"
|
||||
:agent-bot="agentBot"
|
||||
@submit="updateBot"
|
||||
/>
|
||||
<div v-else class="flex flex-col h-auto overflow-auto no-padding">
|
||||
<spinner />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
@@ -43,3 +33,13 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<csml-bot-editor
|
||||
v-if="agentBot.id"
|
||||
:agent-bot="agentBot"
|
||||
@submit="updateBot"
|
||||
/>
|
||||
<div v-else class="flex flex-col h-auto overflow-auto no-padding">
|
||||
<spinner />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<template>
|
||||
<csml-bot-editor :agent-bot="{ bot_config: {} }" @submit="saveBot" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -39,3 +35,7 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<csml-bot-editor :agent-bot="{ bot_config: {} }" @submit="saveBot" />
|
||||
</template>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user