fix: apply ESLint rule vue/component-tags-order

This commit is contained in:
iamsivin
2024-07-25 19:33:28 +05:30
parent fede03ebe8
commit 0008fca511
544 changed files with 21263 additions and 21256 deletions
@@ -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;
@@ -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>
@@ -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;
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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
@@ -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];
@@ -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 {
@@ -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>
@@ -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>
@@ -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;
@@ -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>
@@ -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>
@@ -1,17 +1,3 @@
<template>
<div class="search-input--wrap">
<div class="search-icon--wrap">
<fluent-icon icon="search" size="18" class="search-icon" />
</div>
<input
v-model="searchValue"
class="search-input"
:placeholder="$t('HELP_CENTER.SIDEBAR.SEARCH.PLACEHOLDER')"
@input="onSearch"
/>
</div>
</template>
<script>
export default {
data() {
@@ -27,6 +13,20 @@ export default {
};
</script>
<template>
<div class="search-input--wrap">
<div class="search-icon--wrap">
<fluent-icon icon="search" size="18" class="search-icon" />
</div>
<input
v-model="searchValue"
class="search-input"
:placeholder="$t('HELP_CENTER.SIDEBAR.SEARCH.PLACEHOLDER')"
@input="onSearch"
/>
</div>
</template>
<style lang="scss" scoped>
.search-input--wrap {
display: flex;
@@ -1,3 +1,66 @@
<script>
import { mapGetters } from 'vuex';
import wootConstants from 'dashboard/constants/globals';
export default {
data() {
return {
helpCenterDocsURL: wootConstants.HELP_CENTER_DOCS_URL,
upgradeFeature: [
{
key: 1,
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.PORTALS.TITLE'),
icon: 'book-copy',
description: this.$t(
'HELP_CENTER.UPGRADE_PAGE.FEATURES.PORTALS.DESCRIPTION'
),
},
{
key: 2,
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.LOCALES.TITLE'),
icon: 'globe-line',
description: this.$t(
'HELP_CENTER.UPGRADE_PAGE.FEATURES.LOCALES.DESCRIPTION'
),
},
{
key: 3,
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.SEO.TITLE'),
icon: 'heart-handshake',
description: this.$t(
'HELP_CENTER.UPGRADE_PAGE.FEATURES.SEO.DESCRIPTION'
),
},
{
key: 4,
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.API.TITLE'),
icon: 'search-check',
description: this.$t(
'HELP_CENTER.UPGRADE_PAGE.FEATURES.API.DESCRIPTION'
),
},
],
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', // Pending change text
}),
},
methods: {
openBillingPage() {
this.$router.push({
name: 'billing_settings_index',
params: { accountId: this.accountId },
});
},
openHelpCenterDocs() {
window.open(this.helpCenterDocsURL, '_blank');
},
},
};
</script>
<template>
<div
class="flex flex-col gap-12 sm:gap-16 items-center justify-center py-0 px-4 md:px-0 w-full h-full max-w-full overflow-auto bg-white dark:bg-slate-900"
@@ -70,66 +133,3 @@
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import wootConstants from 'dashboard/constants/globals';
export default {
data() {
return {
helpCenterDocsURL: wootConstants.HELP_CENTER_DOCS_URL,
upgradeFeature: [
{
key: 1,
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.PORTALS.TITLE'),
icon: 'book-copy',
description: this.$t(
'HELP_CENTER.UPGRADE_PAGE.FEATURES.PORTALS.DESCRIPTION'
),
},
{
key: 2,
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.LOCALES.TITLE'),
icon: 'globe-line',
description: this.$t(
'HELP_CENTER.UPGRADE_PAGE.FEATURES.LOCALES.DESCRIPTION'
),
},
{
key: 3,
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.SEO.TITLE'),
icon: 'heart-handshake',
description: this.$t(
'HELP_CENTER.UPGRADE_PAGE.FEATURES.SEO.DESCRIPTION'
),
},
{
key: 4,
title: this.$t('HELP_CENTER.UPGRADE_PAGE.FEATURES.API.TITLE'),
icon: 'search-check',
description: this.$t(
'HELP_CENTER.UPGRADE_PAGE.FEATURES.API.DESCRIPTION'
),
},
],
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', // Pending change text
}),
},
methods: {
openBillingPage() {
this.$router.push({
name: 'billing_settings_index',
params: { accountId: this.accountId },
});
},
openHelpCenterDocs() {
window.open(this.helpCenterDocsURL, '_blank');
},
},
};
</script>
@@ -1,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 {
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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;
@@ -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 {
@@ -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%;
@@ -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;
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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';
@@ -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;
@@ -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;
@@ -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;
@@ -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>
@@ -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