Merge branch 'develop' into feat/CW-3082

This commit is contained in:
Sivin Varghese
2024-04-30 21:44:09 +05:30
committed by GitHub
73 changed files with 2038 additions and 981 deletions
@@ -88,7 +88,6 @@
<script>
import { getUnixTime } from 'date-fns';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
@@ -109,7 +108,7 @@ export default {
WootDropdownMenu,
CustomSnoozeModal,
},
mixins: [clickaway, alertMixin, keyboardEventListenerMixins],
mixins: [alertMixin, keyboardEventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
data() {
return {
@@ -47,7 +47,6 @@
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -67,7 +66,7 @@ export default {
AvailabilityStatusBadge,
},
mixins: [clickaway, alertMixin],
mixins: [alertMixin],
data() {
return {
@@ -106,7 +106,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import Auth from '../../../api/auth';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
@@ -119,7 +118,6 @@ export default {
WootDropdownItem,
AvailabilityStatus,
},
mixins: [clickaway],
props: {
show: {
type: Boolean,
@@ -31,7 +31,7 @@ import CalendarMonth from './components/CalendarMonth.vue';
import CalendarWeek from './components/CalendarWeek.vue';
import CalendarFooter from './components/CalendarFooter.vue';
const { LAST_7_DAYS, CUSTOM_RANGE } = DATE_RANGE_TYPES;
const { LAST_7_DAYS, LAST_30_DAYS, CUSTOM_RANGE } = DATE_RANGE_TYPES;
const { START_CALENDAR, END_CALENDAR } = CALENDAR_TYPES;
const { WEEK, MONTH, YEAR } = CALENDAR_PERIODS;
@@ -59,14 +59,16 @@ const emit = defineEmits(['change']);
// Watcher will set the start and end dates based on the selected range
watch(selectedRange, newRange => {
if (newRange !== CUSTOM_RANGE) {
// If selecting a range other than last 7 days, set the start and end dates to the selected start and end dates
// If selecting last 7 days, set the start date to the selected start date
// If selecting a range other than last 7 days or last 30 days, set the start and end dates to the selected start and end dates
// If selecting last 7 days or last 30 days is, set the start date to the selected start date
// and the end date to one month ahead of the start date if the start date and end date are in the same month
// Otherwise set the end date to the selected end date
const isLast7days = newRange === LAST_7_DAYS;
const isLastSevenOrThirtyDays =
newRange === LAST_7_DAYS || newRange === LAST_30_DAYS;
startCurrentDate.value = selectedStartDate.value;
endCurrentDate.value =
isLast7days && isSameMonth(selectedStartDate.value, selectedEndDate.value)
isLastSevenOrThirtyDays &&
isSameMonth(selectedStartDate.value, selectedEndDate.value)
? startOfMonth(addMonths(selectedStartDate.value, 1))
: selectedEndDate.value;
selectingEndDate.value = false;
@@ -18,13 +18,11 @@
<script>
import { Chrome } from 'vue-color';
import { mixin as clickaway } from 'vue-clickaway';
export default {
components: {
Chrome,
},
mixins: [clickaway],
props: {
value: {
type: String,
@@ -33,7 +33,6 @@
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from 'dashboard/mixins/isAdmin';
export default {
@@ -42,7 +41,7 @@ export default {
LabelDropdown,
},
mixins: [clickaway, adminMixin, keyboardEventListenerMixins],
mixins: [adminMixin, keyboardEventListenerMixins],
props: {
allLabels: {
@@ -45,7 +45,6 @@
<script>
import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
@@ -53,7 +52,7 @@ export default {
components: {
FilterItem,
},
mixins: [clickaway, uiSettingsMixin],
mixins: [uiSettingsMixin],
data() {
return {
showActionsDropdown: false,
@@ -37,7 +37,6 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import EmailTranscriptModal from './EmailTranscriptModal.vue';
import ResolveAction from '../../buttons/ResolveAction.vue';
@@ -52,7 +51,7 @@ export default {
EmailTranscriptModal,
ResolveAction,
},
mixins: [alertMixin, clickaway],
mixins: [alertMixin],
data() {
return {
showEmailActionsModal: false,
@@ -153,7 +153,6 @@
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
@@ -218,7 +217,6 @@ export default {
ArticleSearchPopover,
},
mixins: [
clickaway,
inboxMixin,
uiSettingsMixin,
alertMixin,
@@ -180,7 +180,6 @@
</woot-modal>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import timeMixin from 'dashboard/mixins/time';
@@ -200,7 +199,7 @@ export default {
components: {
Thumbnail,
},
mixins: [keyboardEventListenerMixins, clickaway, timeMixin],
mixins: [keyboardEventListenerMixins, timeMixin],
props: {
show: {
type: Boolean,
@@ -51,7 +51,6 @@
<script>
import { evaluateSLAStatus } from '../helpers/SLAHelper';
import SLAPopoverCard from './SLAPopoverCard.vue';
import { mixin as clickaway } from 'vue-clickaway';
const REFRESH_INTERVAL = 60000;
@@ -59,7 +58,6 @@ export default {
components: {
SLAPopoverCard,
},
mixins: [clickaway],
props: {
chat: {
type: Object,
@@ -101,14 +101,12 @@
import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner.vue';
import { mixin as clickaway } from 'vue-clickaway';
export default {
components: {
Thumbnail,
Spinner,
},
mixins: [clickaway],
props: {
selectedInboxes: {
type: Array,
@@ -74,11 +74,9 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
export default {
mixins: [clickaway],
data() {
return {
query: '',
@@ -59,10 +59,8 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
export default {
mixins: [clickaway],
data() {
return {
query: '',
@@ -36,7 +36,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -45,7 +44,6 @@ export default {
WootDropdownItem,
WootDropdownMenu,
},
mixins: [clickaway],
props: {
selectedInboxes: {
type: Array,
@@ -87,7 +87,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { SHORTCUT_KEYS } from './constants';
import Hotkey from 'dashboard/components/base/Hotkey.vue';
@@ -95,7 +94,6 @@ export default {
components: {
Hotkey,
},
mixins: [clickaway],
props: {
show: {
type: Boolean,
@@ -22,7 +22,7 @@
"body": "Add agents to the created inbox."
},
{
"title": "Voila!",
"title": "Voilà!",
"route": "settings_inbox_finish",
"body": "You are all set to go!"
}
@@ -43,7 +43,7 @@
"CHOOSE_PLACEHOLDER": "Select a page from the list",
"INBOX_NAME": "Inbox Name",
"ADD_NAME": "Add a name for your inbox",
"PICK_NAME": "Pick A Name Your Inbox",
"PICK_NAME": "Pick a Name for your Inbox",
"PICK_A_VALUE": "Pick a value"
},
"TWITTER": {
@@ -62,7 +62,7 @@
},
"CHANNEL_WEBHOOK_URL": {
"LABEL": "Webhook URL",
"PLACEHOLDER": "Enter your Webhook URL",
"PLACEHOLDER": "Please enter your Webhook URL",
"ERROR": "Please enter a valid URL"
},
"CHANNEL_DOMAIN": {
@@ -143,7 +143,7 @@
"ERROR": "This field is required"
},
"PHONE_NUMBER": {
"LABEL": "Phone number",
"LABEL": "Phone Number",
"PLACEHOLDER": "Please enter the phone number from which message will be sent.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
},
@@ -175,12 +175,12 @@
},
"API_KEY": {
"LABEL": "API Key",
"PLACEHOLDER": "Please enter your Bandwith API Key",
"PLACEHOLDER": "Please enter your Bandwidth API Key",
"ERROR": "This field is required"
},
"API_SECRET": {
"LABEL": "API Secret",
"PLACEHOLDER": "Please enter your Bandwith API Secret",
"PLACEHOLDER": "Please enter your Bandwidth API Secret",
"ERROR": "This field is required"
},
"APPLICATION_ID": {
@@ -239,7 +239,7 @@
},
"WEBHOOK_VERIFY_TOKEN": {
"LABEL": "Webhook Verify Token",
"PLACEHOLDER": "Enter a verify token which you want to configure for facebook webhooks.",
"PLACEHOLDER": "Enter a verify token which you want to configure for Facebook webhooks.",
"ERROR": "Please enter a valid value."
},
"API_KEY": {
@@ -269,7 +269,7 @@
},
"WEBHOOK_URL": {
"LABEL": "Webhook URL",
"SUBTITLE": "Configure the URL where you want to recieve callbacks on events.",
"SUBTITLE": "Configure the URL where you want to receive callbacks on events.",
"PLACEHOLDER": "Webhook URL"
},
"SUBMIT_BUTTON": "Create API Channel",
@@ -279,7 +279,7 @@
},
"EMAIL_CHANNEL": {
"TITLE": "Email Channel",
"DESC": "Integrate you email inbox.",
"DESC": "Integrate your email inbox.",
"CHANNEL_NAME": {
"LABEL": "Channel Name",
"PLACEHOLDER": "Please enter a channel name",
@@ -345,7 +345,7 @@
"AGENTS": {
"TITLE": "Agents",
"DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents which are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create.",
"VALIDATION_ERROR": "Add atleast one agent to your new Inbox",
"VALIDATION_ERROR": "Add at least one agent to your new Inbox",
"PICK_AGENTS": "Pick agents for the inbox"
},
"DETAILS": {
@@ -406,7 +406,7 @@
},
"SENDER_NAME_SECTION": {
"TITLE": "Sender name",
"SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.",
"SUB_TEXT": "Select the name shown to your customer when they receive emails from your agents.",
"FOR_EG": "For eg:",
"FRIENDLY": {
"TITLE": "Friendly",
@@ -508,12 +508,12 @@
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved.",
"WHATSAPP_SECTION_SUBHEADER": "This API Key is used for the integration with the WhatsApp APIs.",
"WHATSAPP_SECTION_UPDATE_SUBHEADER": "Enter the updated key to be used for the integration with the WhatsApp APIs.",
"WHATSAPP_SECTION_UPDATE_SUBHEADER": "Enter the new API key to be used for the integration with the WhatsApp APIs.",
"WHATSAPP_SECTION_TITLE": "API Key",
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verify Token",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
},
@@ -585,7 +585,7 @@
"NOTE_TEXT": "To enable SMTP, please configure IMAP.",
"UPDATE": "Update IMAP settings",
"TOGGLE_AVAILABILITY": "Enable IMAP configuration for this inbox",
"TOGGLE_HELP": "Enabling IMAP will help the user to recieve email",
"TOGGLE_HELP": "Enabling IMAP will help the user to receive email",
"EDIT": {
"SUCCESS_MESSAGE": "IMAP settings updated successfully",
"ERROR_MESSAGE": "Unable to update IMAP settings"
@@ -57,13 +57,19 @@
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"NOTE": "This token can be used if you are building an API based integration"
"NOTE": "This token can be used if you are building an API based integration",
"COPY": "Copy"
},
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Audio Notifications",
"NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
"ALERT_TYPES": {
"NONE": "None",
"MINE": "Assigned",
"ALL": "All"
},
"ALERT_TYPE": {
"TITLE": "Alert events:",
"TITLE": "Alert events for conversations:",
"NONE": "None",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
@@ -89,6 +95,22 @@
"SLA_MISSED_NEXT_RESPONSE": "Send email notifications when a conversation misses next response SLA",
"SLA_MISSED_RESOLUTION": "Send email notifications when a conversation misses resolution SLA"
},
"NOTIFICATIONS": {
"TITLE": "Notification preferences",
"TYPE_TITLE": "Notification type",
"EMAIL": "Email",
"PUSH": "Push notification",
"TYPES": {
"CONVERSATION_CREATED": "A new conversation is created",
"CONVERSATION_ASSIGNED": "A conversation is assigned to you",
"CONVERSATION_MENTION": "You are mentioned in a conversation",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "A new message is created in an assigned conversation",
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "A new message is created in a participating conversation",
"SLA_MISSED_FIRST_RESPONSE": "A conversation misses first response SLA",
"SLA_MISSED_NEXT_RESPONSE": "A conversation misses next response SLA",
"SLA_MISSED_RESOLUTION": "A conversation misses resolution SLA"
}
},
"API": {
"UPDATE_SUCCESS": "Your notification preferences are updated successfully",
"UPDATE_ERROR": "There is an error while updating the preferences, please try again"
@@ -106,7 +106,6 @@
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
@@ -124,7 +123,7 @@ export default {
TranslateModal,
MenuItem,
},
mixins: [alertMixin, clickaway, messageFormatterMixin],
mixins: [alertMixin, messageFormatterMixin],
props: {
message: {
type: Object,
@@ -73,7 +73,6 @@ import SearchResultConversationsList from './SearchResultConversationsList.vue';
import SearchResultMessagesList from './SearchResultMessagesList.vue';
import SearchResultContactsList from './SearchResultContactsList.vue';
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default {
@@ -84,7 +83,6 @@ export default {
SearchResultConversationsList,
SearchResultMessagesList,
},
mixins: [clickaway],
data() {
return {
selectedTab: 'all',
@@ -28,7 +28,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { VeTable } from 'vue-easytable';
import { getCountryFlag } from 'dashboard/helper/flag';
@@ -45,7 +44,7 @@ export default {
Spinner,
VeTable,
},
mixins: [clickaway, timeMixin, rtlMixin],
mixins: [timeMixin, rtlMixin],
props: {
contacts: {
type: Array,
@@ -78,7 +78,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
@@ -92,7 +91,7 @@ export default {
ThumbnailGroup,
MultiselectDropdownItems,
},
mixins: [alertMixin, agentMixin, clickaway],
mixins: [alertMixin, agentMixin],
props: {
conversationId: {
type: [Number, String],
@@ -34,7 +34,6 @@
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { mixin as clickaway } from 'vue-clickaway';
import MacroPreview from './MacroPreview.vue';
import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events';
@@ -42,7 +41,7 @@ export default {
components: {
MacroPreview,
},
mixins: [alertMixin, clickaway],
mixins: [alertMixin],
props: {
macro: {
type: Object,
@@ -166,7 +166,6 @@
</div>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import timeMixin from 'dashboard/mixins/time';
import ContactInfoRow from './ContactInfoRow.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
@@ -195,7 +194,7 @@ export default {
NewConversation,
ContactMergeModal,
},
mixins: [alertMixin, adminMixin, clickaway, timeMixin],
mixins: [alertMixin, adminMixin, timeMixin],
props: {
contact: {
type: Object,
@@ -48,7 +48,6 @@ import { mapGetters } from 'vuex';
import Spinner from 'shared/components/Spinner.vue';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from 'dashboard/mixins/isAdmin';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
@@ -60,12 +59,7 @@ export default {
AddLabel,
},
mixins: [
clickaway,
conversationLabelMixin,
adminMixin,
keyboardEventListenerMixins,
],
mixins: [conversationLabelMixin, adminMixin, keyboardEventListenerMixins],
props: {
conversationId: {
type: Number,
@@ -33,7 +33,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
@@ -50,7 +49,7 @@ export default {
},
},
},
mixins: [timeMixin, messageFormatterMixin, clickaway],
mixins: [timeMixin, messageFormatterMixin],
props: {
isOnExpandedLayout: {
type: Boolean,
@@ -34,7 +34,6 @@
<script>
import { debounce } from '@chatwoot/utils';
import { mixin as clickaway } from 'vue-clickaway';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import SearchHeader from './Header.vue';
@@ -52,7 +51,7 @@ export default {
SearchResults,
ArticleView,
},
mixins: [clickaway, portalMixin, alertMixin, keyboardEventListenerMixins],
mixins: [portalMixin, alertMixin, keyboardEventListenerMixins],
props: {
selectedPortalSlug: {
type: String,
@@ -138,8 +138,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
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';
@@ -152,7 +150,6 @@ export default {
WootDropdownMenu,
MultiselectDropdownItems,
},
mixins: [clickaway],
props: {
headerTitle: {
type: String,
@@ -109,14 +109,13 @@
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { mixin as clickaway } from 'vue-clickaway';
import wootConstants from 'dashboard/constants/globals';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
const { ARTICLE_STATUS_TYPES } = wootConstants;
export default {
mixins: [alertMixin, clickaway],
mixins: [alertMixin],
props: {
isSidebarOpen: {
type: Boolean,
@@ -0,0 +1,34 @@
<script setup>
defineProps({
title: {
type: String,
default: null,
},
});
</script>
<template>
<div
class="flex-grow-0 flex-shrink-0 w-full h-full max-w-full bg-white border border-transparent border-solid dark:bg-slate-900 dark:border-transparent md:max-w-2xl"
>
<h3
v-if="$slots.title || title"
class="text-lg text-black-900 dark:text-slate-200"
>
<slot name="title">{{ title }}</slot>
</h3>
<div
class="mx-0 my-4 border-b border-solid border-slate-25 dark:border-slate-800"
>
<slot />
</div>
<div class="flex justify-between">
<div>
<slot name="footer-left" />
</div>
<div>
<slot name="footer-right" />
</div>
</div>
</div>
</template>
@@ -47,13 +47,11 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import PortalSwitch from './PortalSwitch.vue';
export default {
components: {
PortalSwitch,
},
mixins: [clickaway],
props: {
portals: {
type: Array,
@@ -1,256 +1,241 @@
<template>
<div
class="pt-3 bg-white dark:bg-slate-900 h-full border border-solid border-transparent px-6 pb-6 dark:border-transparent w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
>
<div class="w-full">
<h3 class="text-lg text-black-900 dark:text-slate-200">
{{
$t(
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.TITLE'
)
}}
</h3>
</div>
<div
class="my-4 mx-0 border-b border-solid border-slate-25 dark:border-slate-800"
>
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
<div class="mb-4">
<div class="flex items-center flex-row">
<woot-avatar-uploader
ref="imageUpload"
:label="$t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL')"
:src="logoUrl"
@change="onFileChange"
/>
<div v-if="showDeleteButton" class="avatar-delete-btn">
<woot-button
type="button"
color-scheme="alert"
variant="hollow"
size="small"
@click="deleteAvatar"
>
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
</woot-button>
</div>
</div>
<p
class="mt-1 mb-0 text-xs text-slate-600 dark:text-slate-400 not-italic"
>
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.HELP_TEXT') }}
</p>
</div>
<div class="mb-4">
<woot-input
v-model.trim="name"
:class="{ error: $v.name.$error }"
:error="nameError"
:label="$t('HELP_CENTER.PORTAL.ADD.NAME.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.NAME.PLACEHOLDER')"
:help-text="$t('HELP_CENTER.PORTAL.ADD.NAME.HELP_TEXT')"
@blur="$v.name.$touch"
@input="onNameChange"
/>
</div>
<div class="mb-4">
<woot-input
v-model.trim="slug"
:class="{ error: $v.slug.$error }"
:error="slugError"
:label="$t('HELP_CENTER.PORTAL.ADD.SLUG.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.SLUG.PLACEHOLDER')"
:help-text="domainHelpText"
@blur="$v.slug.$touch"
/>
</div>
<div class="mb-4">
<woot-input
v-model.trim="domain"
:class="{ error: $v.domain.$error }"
:label="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.PLACEHOLDER')"
:help-text="domainExampleHelpText"
:error="domainError"
@blur="$v.domain.$touch"
/>
</div>
</div>
</div>
<div class="flex justify-end">
<woot-button
:is-loading="isSubmitting"
:is-disabled="$v.$invalid"
@click="onSubmitClick"
>
{{ submitButtonText }}
</woot-button>
</div>
</div>
</template>
<script setup>
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import { isDomain } from 'shared/helpers/Validators';
import { defineComponent, reactive, computed, onMounted } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useAlert } from 'dashboard/composables';
import alertMixin from 'shared/mixins/alertMixin';
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
import { buildPortalURL } from 'dashboard/helper/portalHelper';
import wootConstants from 'dashboard/constants/globals';
import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import { isDomain } from 'shared/helpers/Validators';
import SettingsLayout from './Layout/SettingsLayout.vue';
const { EXAMPLE_URL } = wootConstants;
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
export default {
mixins: [alertMixin],
props: {
portal: {
type: Object,
default: () => {},
},
isSubmitting: {
type: Boolean,
default: false,
},
submitButtonText: {
type: String,
default: '',
},
},
data() {
return {
name: '',
slug: '',
domain: '',
alertMessage: '',
const { t } = useI18n();
// Logouploader keys
avatarBlobId: '',
logoUrl: '',
};
},
validations: {
name: {
required,
minLength: minLength(2),
},
slug: {
required,
},
domain: {
isDomain,
},
},
computed: {
nameError() {
if (this.$v.name.$error) {
return this.$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR');
}
return '';
},
slugError() {
if (this.$v.slug.$error) {
return this.$t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
}
return '';
},
domainError() {
if (this.$v.domain.$error) {
return this.$t('HELP_CENTER.PORTAL.ADD.DOMAIN.ERROR');
}
return '';
},
domainHelpText() {
return buildPortalURL(this.slug);
},
domainExampleHelpText() {
return this.$t('HELP_CENTER.PORTAL.ADD.DOMAIN.HELP_TEXT', {
exampleURL: EXAMPLE_URL,
});
},
showDeleteButton() {
return hasValidAvatarUrl(this.logoUrl);
},
},
mounted() {
const portal = this.portal || {};
this.name = portal.name || '';
this.slug = portal.slug || '';
this.domain = portal.custom_domain || '';
this.alertMessage = '';
if (portal.logo) {
const {
logo: { file_url: logoURL, blob_id: blobId },
} = portal;
this.logoUrl = logoURL;
this.avatarBlobId = blobId;
}
},
methods: {
onNameChange() {
this.slug = convertToCategorySlug(this.name);
},
onSubmitClick() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
defineComponent({
name: 'PortalSettingsBasicForm',
});
const portal = {
name: this.name,
slug: this.slug,
custom_domain: this.domain,
blob_id: this.avatarBlobId || null,
};
this.$emit('submit', portal);
},
async deleteAvatar() {
this.logoUrl = '';
this.avatarBlobId = '';
this.$emit('delete-logo');
},
onFileChange({ file }) {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadLogoToStorage(file);
} else {
this.showAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
{
size: MAXIMUM_FILE_UPLOAD_SIZE,
}
)
);
}
const props = defineProps({
portal: {
type: Object,
default: () => {},
},
isSubmitting: {
type: Boolean,
default: false,
},
submitButtonText: {
type: String,
default: '',
},
});
this.$refs.imageUpload.value = '';
},
async uploadLogoToStorage(file) {
try {
const { fileUrl, blobId } = await uploadFile(file);
if (fileUrl) {
this.logoUrl = fileUrl;
this.avatarBlobId = blobId;
}
} catch (error) {
this.showAlert(
this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR')
);
}
},
const state = reactive({
name: '',
slug: '',
domain: '',
logoUrl: '',
avatarBlobId: '',
});
const rules = {
name: {
required,
minLength: minLength(2),
},
slug: {
required,
},
domain: {
isDomain,
},
};
</script>
<style lang="scss" scoped>
::v-deep {
input {
@apply mb-1;
const v$ = useVuelidate(rules, state);
const nameError = computed(() => {
if (v$.value.name.$error) {
return t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR');
}
.help-text {
@apply mb-0;
return '';
});
const slugError = computed(() => {
if (v$.value.slug.$error) {
return t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
}
return '';
});
const domainError = computed(() => {
if (v$.value.domain.$error) {
return t('HELP_CENTER.PORTAL.ADD.DOMAIN.ERROR');
}
return '';
});
const domainHelpText = computed(() => {
return buildPortalURL(state.slug);
});
const domainExampleHelpText = computed(() => {
return t('HELP_CENTER.PORTAL.ADD.DOMAIN.HELP_TEXT', {
exampleURL: EXAMPLE_URL,
});
});
const showDeleteButton = computed(() => {
return hasValidAvatarUrl(state.logoUrl);
});
const emit = defineEmits(['submit', 'delete-logo']);
onMounted(() => {
const portal = props.portal || {};
state.name = portal.name || '';
state.slug = portal.slug || '';
state.domain = portal.custom_domain || '';
if (portal.logo) {
const {
logo: { file_url: logoURL, blob_id: blobId },
} = portal;
state.logoUrl = logoURL;
state.avatarBlobId = blobId;
}
});
function onNameChange() {
state.slug = convertToCategorySlug(state.name);
}
function onSubmitClick() {
v$.value.$touch();
if (v$.value.$invalid) {
return;
}
const portal = {
name: state.name,
slug: state.slug,
custom_domain: state.domain,
blob_id: state.avatarBlobId || null,
};
emit('submit', portal);
}
async function deleteAvatar() {
state.logoUrl = '';
state.avatarBlobId = '';
emit('delete-logo');
}
async function uploadLogoToStorage(file) {
try {
const { fileUrl, blobId } = await uploadFile(file);
if (fileUrl) {
state.logoUrl = fileUrl;
state.avatarBlobId = blobId;
}
} catch (error) {
useAlert(t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_UPLOAD_ERROR'));
}
}
</style>
function onFileChange({ file }) {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
uploadLogoToStorage(file);
} else {
const errorKey =
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR';
useAlert(t(errorKey, { size: MAXIMUM_FILE_UPLOAD_SIZE }));
}
}
</script>
<template>
<SettingsLayout
:title="
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.TITLE')
"
>
<div>
<div class="mb-4">
<div class="flex flex-row items-center">
<woot-avatar-uploader
:label="$t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL')"
:src="state.logoUrl"
@change="onFileChange"
/>
<div v-if="showDeleteButton" class="avatar-delete-btn">
<woot-button
type="button"
color-scheme="alert"
variant="hollow"
size="small"
@click="deleteAvatar"
>
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
</woot-button>
</div>
</div>
<p
class="mt-1 mb-0 text-xs not-italic text-slate-600 dark:text-slate-400"
>
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.HELP_TEXT') }}
</p>
</div>
<div class="mb-4">
<woot-input
v-model="state.name"
:class="{ error: v$.name.$error }"
:error="nameError"
:label="$t('HELP_CENTER.PORTAL.ADD.NAME.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.NAME.PLACEHOLDER')"
:help-text="$t('HELP_CENTER.PORTAL.ADD.NAME.HELP_TEXT')"
@blur="v$.name.$touch"
@input="onNameChange"
/>
</div>
<div class="mb-4">
<woot-input
v-model="state.slug"
:class="{ error: v$.slug.$error }"
:error="slugError"
:label="$t('HELP_CENTER.PORTAL.ADD.SLUG.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.SLUG.PLACEHOLDER')"
:help-text="domainHelpText"
@blur="v$.slug.$touch"
/>
</div>
<div class="mb-4">
<woot-input
v-model="state.domain"
:class="{ error: v$.domain.$error }"
:label="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.PLACEHOLDER')"
:help-text="domainExampleHelpText"
:error="domainError"
@blur="v$.domain.$touch"
/>
</div>
</div>
<template #footer-right>
<woot-button
:is-loading="isSubmitting"
:is-disabled="v$.$invalid"
@click="onSubmitClick"
>
{{ submitButtonText }}
</woot-button>
</template>
</SettingsLayout>
</template>
@@ -1,68 +1,140 @@
<script setup>
import { getRandomColor } from 'dashboard/helper/labelColor';
import SettingsLayout from './Layout/SettingsLayout.vue';
import wootConstants from 'dashboard/constants/globals';
const { EXAMPLE_URL } = wootConstants;
import { useVuelidate } from '@vuelidate/core';
import { url } from '@vuelidate/validators';
import { defineComponent, reactive, computed, onMounted } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
defineComponent({
name: 'PortalSettingsCustomizationForm',
});
const { t } = useI18n();
const props = defineProps({
portal: {
type: Object,
default: () => ({}),
},
isSubmitting: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['submit']);
const state = reactive({
color: getRandomColor(),
pageTitle: '',
headerText: '',
homePageLink: '',
});
const rules = {
homePageLink: { url },
};
const homepageExampleHelpText = computed(() => {
return t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.HELP_TEXT', {
exampleURL: EXAMPLE_URL,
});
});
const v$ = useVuelidate(rules, state);
function updateDataFromStore() {
const { portal } = props;
if (portal) {
state.color = portal.color || getRandomColor();
state.pageTitle = portal.page_title || '';
state.headerText = portal.header_text || '';
state.homePageLink = portal.homepage_link || '';
}
}
function onSubmitClick() {
v$.value.$touch();
if (v$.value.$invalid) {
return;
}
const portal = {
id: props.portal.id,
slug: props.portal.slug,
color: state.color,
page_title: state.pageTitle,
header_text: state.headerText,
homepage_link: state.homePageLink,
};
emit('submit', portal);
}
onMounted(() => {
updateDataFromStore();
});
</script>
<template>
<div
class="pt-3 bg-white dark:bg-slate-900 h-full border border-solid border-transparent px-6 pb-6 dark:border-transparent w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
<SettingsLayout
:title="
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.CUSTOMIZATION_PAGE.TITLE')
"
>
<div class="w-full">
<h3 class="text-lg text-black-900 dark:text-slate-200">
{{
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.CUSTOMIZATION_PAGE.TITLE')
}}
</h3>
</div>
<div
class="my-4 mx-0 border-b border-solid border-slate-25 dark:border-slate-800"
>
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
<div class="mb-4">
<label>
{{ $t('HELP_CENTER.PORTAL.ADD.THEME_COLOR.LABEL') }}
</label>
<woot-color-picker v-model="color" />
<p
class="mt-1 mb-0 text-xs text-slate-600 dark:text-slate-400 not-italic"
>
{{ $t('HELP_CENTER.PORTAL.ADD.THEME_COLOR.HELP_TEXT') }}
</p>
</div>
<div class="mb-4">
<woot-input
v-model.trim="pageTitle"
:label="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.PLACEHOLDER')"
:help-text="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.HELP_TEXT')"
/>
</div>
<div class="mb-4">
<woot-input
v-model.trim="headerText"
:label="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.PLACEHOLDER')"
:help-text="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.HELP_TEXT')"
/>
</div>
<div class="mb-4">
<woot-input
v-model.trim="homePageLink"
:label="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.LABEL')"
:placeholder="
$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.PLACEHOLDER')
"
:help-text="homepageExampleHelpText"
:error="
$v.homePageLink.$error
? $t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.ERROR')
: ''
"
:class="{ error: $v.homePageLink.$error }"
@blur="$v.homePageLink.$touch"
/>
</div>
<div class="flex-grow-0 flex-shrink-0">
<div class="mb-4">
<label>
{{ $t('HELP_CENTER.PORTAL.ADD.THEME_COLOR.LABEL') }}
</label>
<woot-color-picker v-model="state.color" />
<p
class="mt-1 mb-0 text-xs not-italic text-slate-600 dark:text-slate-400"
>
{{ $t('HELP_CENTER.PORTAL.ADD.THEME_COLOR.HELP_TEXT') }}
</p>
</div>
<div class="mb-4">
<woot-input
v-model="state.pageTitle"
:label="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.PLACEHOLDER')"
:help-text="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.HELP_TEXT')"
/>
</div>
<div class="mb-4">
<woot-input
v-model="state.headerText"
:label="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.PLACEHOLDER')"
:help-text="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.HELP_TEXT')"
/>
</div>
<div class="mb-4">
<woot-input
v-model="state.homePageLink"
:label="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.LABEL')"
:placeholder="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.PLACEHOLDER')"
:help-text="homepageExampleHelpText"
:error="
v$.homePageLink.$error
? $t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.ERROR')
: ''
"
:class="{ error: v$.homePageLink.$error }"
@blur="v$.homePageLink.$touch"
/>
</div>
</div>
<div class="flex justify-end">
<template #footer-right>
<woot-button
:is-loading="isSubmitting"
:is-disabled="$v.$invalid"
:is-disabled="v$.$invalid"
@click="onSubmitClick"
>
{{
@@ -71,94 +143,12 @@
)
}}
</woot-button>
</div>
</div>
</template>
</SettingsLayout>
</template>
<script>
import { url } from 'vuelidate/lib/validators';
import { getRandomColor } from 'dashboard/helper/labelColor';
import alertMixin from 'shared/mixins/alertMixin';
import wootConstants from 'dashboard/constants/globals';
const { EXAMPLE_URL } = wootConstants;
export default {
components: {},
mixins: [alertMixin],
props: {
portal: {
type: Object,
default: () => ({}),
},
isSubmitting: {
type: Boolean,
default: false,
},
},
data() {
return {
color: '#000',
pageTitle: '',
headerText: '',
homePageLink: '',
alertMessage: '',
};
},
validations: {
homePageLink: {
url,
},
},
computed: {
homepageExampleHelpText() {
return this.$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.HELP_TEXT', {
exampleURL: EXAMPLE_URL,
});
},
},
mounted() {
this.color = getRandomColor();
this.updateDataFromStore();
},
methods: {
updateDataFromStore() {
const { portal } = this;
if (portal) {
this.color = portal.color || getRandomColor();
this.pageTitle = portal.page_title || '';
this.headerText = portal.header_text || '';
this.homePageLink = portal.homepage_link || '';
this.alertMessage = '';
}
},
onSubmitClick() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
const portal = {
id: this.portal.id,
slug: this.portal.slug,
color: this.color,
page_title: this.pageTitle,
header_text: this.headerText,
homepage_link: this.homePageLink,
};
this.$emit('submit', portal);
},
},
};
</script>
<style lang="scss" scoped>
::v-deep {
input {
@apply mb-1;
}
.help-text {
@apply mb-0;
}
.colorpicker--selected {
@apply mb-0;
}
@@ -1,15 +1,115 @@
<script setup>
import { useRoute } from 'dashboard/composables/route';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
import { useAlert, useTrack } from 'dashboard/composables';
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { defineComponent, ref, computed } from 'vue';
import CategoryListItem from './CategoryListItem.vue';
import AddCategory from './AddCategory.vue';
import EditCategory from './EditCategory.vue';
defineComponent({
name: 'ListAllCategories',
});
const selectedCategory = ref({});
const currentLocaleCode = ref('en');
const showEditCategoryModal = ref(false);
const showAddCategoryModal = ref(false);
const getters = useStoreGetters();
const store = useStore();
const route = useRoute();
const track = useTrack();
const { t } = useI18n();
const currentPortalSlug = computed(() => {
return route.params.portalSlug;
});
const categoriesByLocaleCode = computed(() => {
return getters['categories/categoriesByLocaleCode'].value(
currentLocaleCode.value
);
});
const currentPortal = computed(() => {
const slug = currentPortalSlug.value;
if (slug) return getters['portals/portalBySlug'].value(slug);
return getters['portals/allPortals'].value[0];
});
const currentPortalName = computed(() => {
return currentPortal.value ? currentPortal.value.name : '';
});
const allLocales = computed(() => {
return currentPortal.value ? currentPortal.value.config.allowed_locales : [];
});
const allowedLocaleCodes = computed(() => {
return allLocales.value.map(locale => locale.code);
});
function openAddCategoryModal() {
showAddCategoryModal.value = true;
}
function openEditCategoryModal(category) {
selectedCategory.value = category;
showEditCategoryModal.value = true;
}
function closeAddCategoryModal() {
showAddCategoryModal.value = false;
}
function closeEditCategoryModal() {
showEditCategoryModal.value = false;
}
async function fetchCategoriesByPortalSlugAndLocale(localeCode) {
await store.dispatch('categories/index', {
portalSlug: currentPortalSlug.value,
locale: localeCode,
});
}
async function deleteCategory(category) {
let alertMessage = '';
try {
await store.dispatch('categories/delete', {
portalSlug: currentPortalSlug.value,
categoryId: category.id,
});
alertMessage = t('HELP_CENTER.CATEGORY.DELETE.API.SUCCESS_MESSAGE');
track(PORTALS_EVENTS.DELETE_CATEGORY, {
hasArticles: category?.meta?.articles_count !== 0,
});
} catch (error) {
const errorMessage = error?.message;
alertMessage =
errorMessage || t('HELP_CENTER.CATEGORY.DELETE.API.ERROR_MESSAGE');
} finally {
useAlert(alertMessage);
}
}
function changeCurrentCategory(event) {
const localeCode = event.target.value;
currentLocaleCode.value = localeCode;
fetchCategoriesByPortalSlugAndLocale(localeCode);
}
</script>
<template>
<div class="w-full pl-4">
<header class="flex justify-between items-center mb-4">
<div class="w-full max-w-6xl">
<header class="flex items-center justify-between mb-4">
<div class="flex items-center w-full gap-3">
<label
class="font-normal mb-0 text-base text-slate-800 dark:text-slate-100"
class="mb-0 text-base font-normal text-slate-800 dark:text-slate-100"
>
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TITLE') }}
</label>
<select
:value="currentLocaleCode"
class="w-[15%] select-locale"
class="w-[15%] h-8 mb-0 py-0.5"
@change="changeCurrentCategory"
>
<option
@@ -21,7 +121,7 @@
</option>
</select>
</div>
<div class="flex-none items-center">
<div class="items-center flex-none">
<woot-button
size="small"
variant="smooth"
@@ -58,119 +158,3 @@
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import portalMixin from '../../mixins/portalMixin';
import CategoryListItem from './CategoryListItem.vue';
import AddCategory from './AddCategory.vue';
import EditCategory from './EditCategory.vue';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
components: {
CategoryListItem,
AddCategory,
EditCategory,
},
mixins: [alertMixin, portalMixin],
data() {
return {
selectedCategory: {},
selectedLocaleCode: '',
currentLocaleCode: 'en',
showEditCategoryModal: false,
showAddCategoryModal: false,
alertMessage: '',
};
},
computed: {
...mapGetters({
portals: 'portals/allPortals',
meta: 'portals/getMeta',
isFetching: 'portals/isFetchingPortals',
}),
currentPortalSlug() {
return this.$route.params.portalSlug;
},
categoriesByLocaleCode() {
return this.$store.getters['categories/categoriesByLocaleCode'](
this.currentLocaleCode
);
},
currentPortal() {
const slug = this.currentPortalSlug;
if (slug) return this.$store.getters['portals/portalBySlug'](slug);
return this.$store.getters['portals/allPortals'][0];
},
currentPortalName() {
return this.currentPortal ? this.currentPortal.name : '';
},
currentPortalLocale() {
return this.currentPortal ? this.currentPortal?.meta?.default_locale : '';
},
allLocales() {
return this.currentPortal
? this.currentPortal.config.allowed_locales
: [];
},
allowedLocaleCodes() {
return this.allLocales.map(locale => locale.code);
},
},
methods: {
openAddCategoryModal() {
this.showAddCategoryModal = true;
},
openEditCategoryModal(category) {
this.selectedCategory = category;
this.showEditCategoryModal = true;
},
closeAddCategoryModal() {
this.showAddCategoryModal = false;
},
closeEditCategoryModal() {
this.showEditCategoryModal = false;
},
async fetchCategoriesByPortalSlugAndLocale(localeCode) {
await this.$store.dispatch('categories/index', {
portalSlug: this.currentPortalSlug,
locale: localeCode,
});
},
async deleteCategory(category) {
try {
await this.$store.dispatch('categories/delete', {
portalSlug: this.currentPortalSlug,
categoryId: category.id,
});
this.alertMessage = this.$t(
'HELP_CENTER.CATEGORY.DELETE.API.SUCCESS_MESSAGE'
);
this.$track(PORTALS_EVENTS.DELETE_CATEGORY, {
hasArticles: category?.meta?.articles_count !== 0,
});
} catch (error) {
const errorMessage = error?.message;
this.alertMessage =
errorMessage ||
this.$t('HELP_CENTER.CATEGORY.DELETE.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
}
},
changeCurrentCategory(event) {
const localeCode = event.target.value;
this.currentLocaleCode = localeCode;
this.fetchCategoriesByPortalSlugAndLocale(localeCode);
},
},
};
</script>
<style lang="scss" scoped>
.select-locale {
@apply h-8 mb-0 py-0.5;
}
</style>
@@ -36,13 +36,10 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
export default {
components: { EmojiInput },
mixins: [clickaway],
props: {
label: {
type: String,
@@ -24,7 +24,7 @@
/>
</woot-tabs>
</setting-intro-banner>
<div class="p-4 max-w-full my-auto flex flex-wrap">
<div class="flex flex-wrap max-w-full px-8 py-4 my-auto">
<router-view />
</div>
</div>
@@ -1,3 +1,75 @@
<script setup>
import PortalSettingsBasicForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue';
import { useAlert } from 'dashboard/composables';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { useRoute, useRouter } from 'dashboard/composables/route';
import { useI18n } from 'dashboard/composables/useI18n';
import { defineComponent, computed, ref, onMounted } from 'vue';
defineComponent({ name: 'EditPortalBasic' });
const getters = useStoreGetters();
const route = useRoute();
const router = useRouter();
const store = useStore();
const { t } = useI18n();
const uiFlags = getters['portals/uiFlagsIn'];
const lastPortalSlug = ref(null);
const currentPortalSlug = computed(() => {
return route.params.portalSlug;
});
const currentPortal = computed(() => {
const slug = route.params.portalSlug;
return getters['portals/portalBySlug'].value(slug);
});
onMounted(() => {
lastPortalSlug.value = currentPortalSlug.value;
});
async function updatePortalSettings(portalObj) {
let alertMessage = '';
try {
const portalSlug = lastPortalSlug.value;
await store.dispatch('portals/update', { ...portalObj, portalSlug });
alertMessage = t('HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE');
if (lastPortalSlug.value !== portalObj.slug) {
await store.dispatch('portals/index');
router.replace({
name: route.name,
params: { portalSlug: portalObj.slug },
});
}
} catch (error) {
alertMessage =
error?.message ||
t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
} finally {
useAlert(alertMessage);
}
}
async function deleteLogo() {
try {
const portalSlug = lastPortalSlug.value;
await store.dispatch('portals/deleteLogo', {
portalSlug,
});
} catch (error) {
useAlert(
error?.message || t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR')
);
}
}
</script>
<template>
<portal-settings-basic-form
v-if="currentPortal"
@@ -10,80 +82,3 @@
@delete-logo="deleteLogo"
/>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import PortalSettingsBasicForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue';
export default {
components: {
PortalSettingsBasicForm,
},
mixins: [alertMixin],
data() {
return {
lastPortalSlug: undefined,
alertMessage: '',
};
},
computed: {
...mapGetters({
uiFlags: 'portals/uiFlagsIn',
}),
currentPortalSlug() {
return this.$route.params.portalSlug;
},
currentPortal() {
return this.$store.getters['portals/portalBySlug'](
this.currentPortalSlug
);
},
},
mounted() {
this.lastPortalSlug = this.currentPortalSlug;
},
methods: {
async updatePortalSettings(portalObj) {
try {
const portalSlug = this.lastPortalSlug;
await this.$store.dispatch('portals/update', {
...portalObj,
portalSlug,
});
this.alertMessage = this.$t(
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE'
);
if (this.lastPortalSlug !== portalObj.slug) {
await this.$store.dispatch('portals/index');
this.$router.replace({
name: this.$route.name,
params: { portalSlug: portalObj.slug },
});
}
} catch (error) {
this.alertMessage =
error?.message ||
this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
} finally {
this.showAlert(this.alertMessage);
}
},
async deleteLogo() {
try {
const portalSlug = this.lastPortalSlug;
await this.$store.dispatch('portals/deleteLogo', {
portalSlug,
});
} catch (error) {
this.alertMessage =
error?.message ||
this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR');
this.showAlert(this.alertMessage);
}
},
},
};
</script>
@@ -1,3 +1,48 @@
<script setup>
import PortalSettingsCustomizationForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue';
import { useAlert } from 'dashboard/composables';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { useRoute } from 'dashboard/composables/route';
import { useI18n } from 'dashboard/composables/useI18n';
import { defineComponent, computed } from 'vue';
defineComponent({
name: 'EditPortalCustomization',
});
const getters = useStoreGetters();
const route = useRoute();
const store = useStore();
const { t } = useI18n();
const uiFlags = getters['portals/uiFlagsIn'];
const currentPortal = computed(() => {
const slug = route.params.portalSlug;
return getters['portals/portalBySlug'].value(slug);
});
async function updatePortalSettings(portalObj) {
const portalSlug = route.params.portalSlug;
let alertMessage = '';
try {
await store.dispatch('portals/update', {
...portalObj,
portalSlug,
});
alertMessage = t('HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE');
} catch (error) {
alertMessage =
error?.message ||
t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
} finally {
useAlert(alertMessage);
}
}
</script>
<template>
<portal-settings-customization-form
v-if="currentPortal"
@@ -9,51 +54,3 @@
@submit="updatePortalSettings"
/>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import PortalSettingsCustomizationForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue';
import { mapGetters } from 'vuex';
export default {
components: {
PortalSettingsCustomizationForm,
},
mixins: [alertMixin],
data() {
return {
alertMessage: '',
};
},
computed: {
...mapGetters({
uiFlags: 'portals/uiFlagsIn',
}),
currentPortal() {
const slug = this.$route.params.portalSlug;
return this.$store.getters['portals/portalBySlug'](slug);
},
},
methods: {
async updatePortalSettings(portalObj) {
const portalSlug = this.$route.params.portalSlug;
try {
await this.$store.dispatch('portals/update', {
...portalObj,
portalSlug,
});
this.alertMessage = this.$t(
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE'
);
} catch (error) {
this.alertMessage =
error?.message ||
this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
} finally {
this.showAlert(this.alertMessage);
}
},
},
};
</script>
@@ -1,6 +1,117 @@
<script setup>
import LocaleItemTable from 'dashboard/routes/dashboard/helpcenter/components/PortalListItemTable.vue';
import AddLocale from 'dashboard/routes/dashboard/helpcenter/components/AddLocale.vue';
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { useAlert, useTrack } from 'dashboard/composables';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { useRoute } from 'dashboard/composables/route';
import { useI18n } from 'dashboard/composables/useI18n';
import { defineComponent, ref, onBeforeMount, computed } from 'vue';
defineComponent({
name: 'EditPortalLocales',
});
const isAddLocaleModalOpen = ref(false);
const getters = useStoreGetters();
const store = useStore();
const route = useRoute();
const track = useTrack();
const { t } = useI18n();
const currentPortalSlug = computed(() => {
return route.params.portalSlug;
});
const currentPortal = computed(() => {
const slug = currentPortalSlug.value;
if (slug) return getters['portals/portalBySlug'].value(slug);
return getters['portals/allPortals'].value[0];
});
const locales = computed(() => {
return currentPortal.value?.config.allowed_locales;
});
const allowedLocales = computed(() => {
return Object.keys(locales.value).map(key => {
return this.locales.value[key].code;
});
});
async function fetchPortals() {
await store.dispatch('portals/index');
}
onBeforeMount(() => {
fetchPortals();
});
async function updatePortalLocales({
newAllowedLocales,
defaultLocale,
messageKey,
}) {
let alertMessage = '';
try {
await store.dispatch('portals/update', {
portalSlug: currentPortalSlug.value,
config: {
default_locale: defaultLocale,
allowed_locales: newAllowedLocales,
},
});
alertMessage = t(`HELP_CENTER.PORTAL.${messageKey}.API.SUCCESS_MESSAGE`);
} catch (error) {
alertMessage =
error?.message || t(`HELP_CENTER.PORTAL.${messageKey}.API.ERROR_MESSAGE`);
} finally {
useAlert(alertMessage);
}
}
function changeDefaultLocale({ localeCode }) {
updatePortalLocales({
allowedLocales: allowedLocales.value,
defaultLocale: localeCode,
messageKey: 'CHANGE_DEFAULT_LOCALE',
});
track(PORTALS_EVENTS.SET_DEFAULT_LOCALE, {
newLocale: localeCode,
from: route.name,
});
}
function deletePortalLocale({ localeCode }) {
const updatedLocales = allowedLocales.value.filter(
code => code !== localeCode
);
const defaultLocale = currentPortal.value?.meta.default_locale;
updatePortalLocales({
allowedLocales: updatedLocales,
defaultLocale,
messageKey: 'DELETE_LOCALE',
});
track(PORTALS_EVENTS.DELETE_LOCALE, {
deletedLocale: localeCode,
from: route.name,
});
}
function closeAddLocaleModal() {
isAddLocaleModalOpen.value = false;
}
function addLocale() {
isAddLocaleModalOpen.value = true;
}
</script>
<template>
<div class="portal-locales">
<div class="button-container">
<div class="w-full h-full max-w-6xl space-y-4 bg-white dark:bg-slate-900">
<div class="flex justify-end">
<woot-button
variant="smooth"
size="small"
@@ -11,19 +122,18 @@
{{ $t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.ADD') }}
</woot-button>
</div>
<div class="locale-container">
<locale-item-table
:locales="locales"
:selected-locale-code="currentPortal.meta.default_locale"
@change-default-locale="changeDefaultLocale"
@delete="deletePortalLocale"
/>
</div>
<LocaleItemTable
v-if="currentPortal"
:locales="locales"
:selected-locale-code="currentPortal.meta.default_locale"
@change-default-locale="changeDefaultLocale"
@delete="deletePortalLocale"
/>
<woot-modal
:show.sync="isAddLocaleModalOpen"
:on-close="closeAddLocaleModal"
>
<add-locale
<AddLocale
:show="isAddLocaleModalOpen"
:portal="currentPortal"
@cancel="closeAddLocaleModal"
@@ -31,126 +141,3 @@
</woot-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import LocaleItemTable from 'dashboard/routes/dashboard/helpcenter/components/PortalListItemTable.vue';
import AddLocale from 'dashboard/routes/dashboard/helpcenter/components/AddLocale.vue';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
components: {
LocaleItemTable,
AddLocale,
},
mixins: [alertMixin],
data() {
return {
isAddLocaleModalOpen: false,
lastPortalSlug: undefined,
alertMessage: '',
};
},
computed: {
...mapGetters({
uiFlags: 'portals/uiFlagsIn',
}),
currentPortalSlug() {
return this.$route.params.portalSlug;
},
currentPortal() {
return this.$store.getters['portals/portalBySlug'](
this.currentPortalSlug
);
},
locales() {
return this.currentPortal.config.allowed_locales;
},
allowedLocales() {
return Object.keys(this.locales).map(key => {
return this.locales[key].code;
});
},
},
mounted() {
this.lastPortalSlug = this.currentPortalSlug;
},
methods: {
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.currentPortal.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.currentPortal.slug,
config: {
default_locale: defaultLocale,
allowed_locales: allowedLocales,
},
});
this.alertMessage = successMessage;
} catch (error) {
this.alertMessage = error?.message || errorMessage;
} finally {
this.showAlert(this.alertMessage);
}
},
closeAddLocaleModal() {
this.isAddLocaleModalOpen = false;
this.selectedPortal = {};
},
addLocale() {
this.isAddLocaleModalOpen = true;
},
},
};
</script>
<style lang="scss" scoped>
.portal-locales {
@apply w-full bg-white dark:bg-slate-900 h-full py-0 pr-0 pl-4;
.button-container {
@apply flex justify-end;
}
.locale-container {
@apply mt-4;
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="flex-1">
<section class="flex-1">
<settings-header
button-route="new"
:header-title="portalHeaderText"
@@ -10,16 +10,20 @@
:show-new-button="false"
/>
<div
class="flex flex-row overflow-auto py-4 pl-4 rtl:pl-0 rtl:pr-4 h-full bg-slate-50 dark:bg-slate-800"
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 w-1/4"
class="hidden md:block"
:global-config="globalConfig"
:items="items"
/>
<router-view />
<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>
</div>
</section>
</template>
<script>
@@ -1,3 +1,63 @@
<script setup>
import PortalSettingsCustomizationForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue';
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { useAlert, useTrack } from 'dashboard/composables';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { useRoute, useRouter } from 'dashboard/composables/route';
import { useI18n } from 'dashboard/composables/useI18n';
import { defineComponent, onMounted, computed } from 'vue';
defineComponent({
name: 'PortalCustomization',
});
const getters = useStoreGetters();
const route = useRoute();
const router = useRouter();
const store = useStore();
const track = useTrack();
const { t } = useI18n();
const uiFlags = getters['portals/uiFlagsIn'];
const currentPortal = computed(() => {
const slug = route.params.portalSlug;
if (slug) return getters['portals/portalBySlug'].value(slug);
return {};
});
onMounted(() => {
store.dispatch('portals/index');
});
async function updatePortalSettings(portalObj) {
const portalSlug = route.params.portalSlug;
let alertMessage = '';
try {
await store.dispatch('portals/update', {
portalSlug,
...portalObj,
});
alertMessage = t('HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE');
track(PORTALS_EVENTS.ONBOARD_CUSTOMIZATION, {
hasHomePageLink: Boolean(portalObj.homepage_link),
hasPageTitle: Boolean(portalObj.page_title),
hasHeaderText: Boolean(portalObj.headerText),
});
} catch (error) {
alertMessage =
error?.message ||
t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
} finally {
useAlert(alertMessage);
router.push({ name: 'portal_finish' });
}
}
</script>
<template>
<portal-settings-customization-form
v-if="currentPortal"
@@ -9,75 +69,3 @@
@submit="updatePortalSettings"
/>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import PortalSettingsCustomizationForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue';
import { mapGetters } from 'vuex';
import { getRandomColor } from 'dashboard/helper/labelColor';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
export default {
components: {
PortalSettingsCustomizationForm,
},
mixins: [alertMixin],
data() {
return {
color: '#000',
pageTitle: '',
headerText: '',
homePageLink: '',
alertMessage: '',
};
},
computed: {
...mapGetters({
uiFlags: 'portals/uiFlagsIn',
portals: 'portals/allPortals',
}),
currentPortal() {
const slug = this.$route.params.portalSlug;
return this.$store.getters['portals/portalBySlug'](slug);
},
},
mounted() {
this.fetchPortals();
this.color = getRandomColor();
},
methods: {
fetchPortals() {
this.$store.dispatch('portals/index');
},
async updatePortalSettings(portalObj) {
const portalSlug = this.$route.params.portalSlug;
try {
await this.$store.dispatch('portals/update', {
portalSlug,
...portalObj,
});
this.alertMessage = this.$t(
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE'
);
this.$track(PORTALS_EVENTS.ONBOARD_CUSTOMIZATION, {
hasHomePageLink: Boolean(portalObj.homepage_link),
hasPageTitle: Boolean(portalObj.page_title),
hasHeaderText: Boolean(portalObj.headerText),
});
} catch (error) {
this.alertMessage =
error?.message ||
this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
} finally {
this.showAlert(this.alertMessage);
this.$router.push({
name: 'portal_finish',
});
}
},
},
};
</script>
@@ -1,6 +1,6 @@
<template>
<div
class="pt-3 bg-white dark:bg-slate-900 h-full border border-solid border-transparent px-6 pb-6 dark:border-transparent w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
class="flex-grow-0 flex-shrink-0 w-full h-full max-w-full px-6 pt-3 pb-6 bg-white border border-transparent border-solid dark:bg-slate-900 dark:border-transparent"
>
<empty-state
:title="$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.FINISH_PAGE.TITLE')"
@@ -10,7 +10,7 @@
>
<div class="w-full text-center">
<router-link
class="button success nice rounded"
class="rounded button success nice"
:to="{
name: 'list_all_portals',
}"
@@ -22,18 +22,10 @@
</div>
</template>
<script>
<script setup>
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
export default {
components: {
EmptyState,
},
methods: {
changeRoute() {
this.$router.push({
name: 'list_all_portals',
});
},
},
};
import { defineComponent } from 'vue';
defineComponent({
name: 'PortalSettingsFinish',
});
</script>
@@ -57,7 +57,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import InboxOptionMenu from './InboxOptionMenu.vue';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import InboxDisplayMenu from './InboxDisplayMenu.vue';
@@ -68,7 +67,7 @@ export default {
InboxOptionMenu,
InboxDisplayMenu,
},
mixins: [clickaway, alertMixin],
mixins: [alertMixin],
props: {
isContextMenuOpen: {
type: Boolean,
@@ -115,7 +115,6 @@
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import rtlMixin from 'shared/mixins/rtlMixin';
import NotificationPanelList from './NotificationPanelList.vue';
@@ -125,7 +124,7 @@ export default {
components: {
NotificationPanelList,
},
mixins: [clickaway, rtlMixin],
mixins: [rtlMixin],
data() {
return {
pageSize: 15,
@@ -21,7 +21,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import campaignMixin from 'shared/mixins/campaignMixin';
@@ -34,7 +33,7 @@ export default {
CampaignCard,
},
mixins: [clickaway, campaignMixin],
mixins: [campaignMixin],
props: {
campaigns: {
@@ -0,0 +1,42 @@
<template>
<div>
<label
class="flex justify-between pb-1 text-sm font-medium leading-6 text-ash-900"
>
{{ label }}
</label>
<div class="flex flex-col gap-4">
<div
v-for="item in items"
:key="item.id"
class="flex flex-row items-start gap-2"
>
<CheckBox
:is-checked="item.model"
:value="item.value"
@update="onChange"
/>
<label class="text-sm font-normal text-ash-900">
{{ item.label }}
</label>
</div>
</div>
</div>
</template>
<script setup>
import CheckBox from 'v3/components/Form/CheckBox.vue';
defineProps({
label: {
type: String,
required: true,
},
items: {
type: Array,
required: true,
},
});
const emit = defineEmits(['change']);
const onChange = (id, value) => {
emit('change', id, value);
};
</script>
@@ -0,0 +1,66 @@
<template>
<div>
<label
class="flex justify-between pb-1 text-sm font-medium leading-6 text-ash-900"
>
{{ label }}
</label>
<div
class="flex flex-row justify-between h-10 max-w-xl p-2 border border-solid rounded-xl border-ash-200"
>
<div
v-for="option in alertEvents"
:key="option.value"
class="flex flex-row items-center justify-center gap-2 px-4 border-r border-ash-200 grow last:border-r-0"
>
<input
:id="`radio-${option.value}`"
v-model="selectedValue"
class="shadow cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-4 h-4 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[14px] checked:before:h-[14px] checked:border checked:border-primary-600"
type="radio"
:value="option.value"
/>
<label
:for="`radio-${option.value}`"
class="text-sm font-medium"
:class="
selectedValue === option.value ? 'text-ash-900' : 'text-ash-800'
"
>
{{
$t(
`PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPES.${option.label.toUpperCase()}`
)
}}
</label>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { ALERT_EVENTS } from './constants';
const props = defineProps({
label: {
type: String,
default: '',
},
value: {
type: String,
default: 'all',
},
});
const alertEvents = ALERT_EVENTS;
const emit = defineEmits(['update']);
const selectedValue = computed({
get: () => props.value,
set: value => {
emit('update', value);
},
});
</script>
@@ -0,0 +1,56 @@
<template>
<form-select
v-model="selectedValue"
name="alertTone"
spacing="compact"
:value="selectedValue"
:options="alertTones"
:label="label"
class="max-w-xl"
>
<option
v-for="tone in alertTones"
:key="tone.label"
:value="tone.value"
:selected="tone.value === selectedValue"
>
{{ tone.label }}
</option>
</form-select>
</template>
<script setup>
import { computed } from 'vue';
import FormSelect from 'v3/components/Form/Select.vue';
const props = defineProps({
value: {
type: String,
required: true,
validator: value => ['ding', 'bell'].includes(value),
},
label: {
type: String,
default: '',
},
});
const alertTones = computed(() => [
{
value: 'ding',
label: 'Ding',
},
{
value: 'bell',
label: 'Bell',
},
]);
const emit = defineEmits(['change']);
const selectedValue = computed({
get: () => props.value,
set: value => {
emit('change', value);
},
});
</script>
@@ -0,0 +1,128 @@
<template>
<div id="profile-settings-notifications" class="flex flex-col gap-6">
<audio-alert-tone
:value="alertTone"
:label="
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.DEFAULT_TONE.TITLE'
)
"
@change="handleAudioToneChange"
/>
<audio-alert-event
:label="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.TITLE')
"
:value="audioAlert"
@update="handAudioAlertChange"
/>
<audio-alert-condition
:items="audioAlertConditions"
:label="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.TITLE')
"
@change="handleAudioAlertConditions"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import configMixin from 'shared/mixins/configMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AudioAlertTone from './AudioAlertTone.vue';
import AudioAlertEvent from './AudioAlertEvent.vue';
import AudioAlertCondition from './AudioAlertCondition.vue';
export default {
components: {
AudioAlertEvent,
AudioAlertTone,
AudioAlertCondition,
},
mixins: [alertMixin, configMixin, uiSettingsMixin],
data() {
return {
audioAlert: '',
playAudioWhenTabIsInactive: false,
alertIfUnreadConversationExist: false,
alertTone: 'ding',
audioAlertConditions: [],
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
uiSettings: 'getUISettings',
}),
},
watch: {
uiSettings(value) {
this.notificationUISettings(value);
},
},
mounted() {
this.notificationUISettings(this.uiSettings);
this.$store.dispatch('userNotificationSettings/get');
},
methods: {
notificationUISettings(uiSettings) {
const {
enable_audio_alerts: audioAlert = '',
always_play_audio_alert: alwaysPlayAudioAlert,
alert_if_unread_assigned_conversation_exist:
alertIfUnreadConversationExist,
notification_tone: alertTone,
} = uiSettings;
this.audioAlert = audioAlert;
this.playAudioWhenTabIsInactive = !alwaysPlayAudioAlert;
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
this.audioAlertConditions = [
{
id: 'audio1',
label: this.$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_ONE'
),
model: this.playAudioWhenTabIsInactive,
value: 'tab_is_inactive',
},
{
id: 'audio2',
label: this.$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_TWO'
),
model: this.alertIfUnreadConversationExist,
value: 'conversations_are_read',
},
];
this.alertTone = alertTone || 'ding';
},
handAudioAlertChange(value) {
this.audioAlert = value;
this.updateUISettings({
enable_audio_alerts: this.audioAlert,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioAlertConditions(id, value) {
if (id === 'tab_is_inactive') {
this.updateUISettings({
always_play_audio_alert: !value,
});
} else if (id === 'conversations_are_read') {
this.updateUISettings({
alert_if_unread_assigned_conversation_exist: value,
});
}
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioToneChange(value) {
this.updateUISettings({ notification_tone: value });
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
},
};
</script>
@@ -0,0 +1,137 @@
<template>
<form @submit.prevent="changePassword()">
<div class="flex flex-col w-full gap-4">
<woot-input
v-model="currentPassword"
type="password"
:styles="inputStyles"
:class="{ error: $v.currentPassword.$error }"
:label="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.PLACEHOLDER')"
:error="`${
$v.currentPassword.$error
? $t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.ERROR')
: ''
}`"
@input="$v.currentPassword.$touch"
/>
<woot-input
v-model="password"
type="password"
:styles="inputStyles"
:class="{ error: $v.password.$error }"
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
:error="`${
$v.password.$error ? $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') : ''
}`"
@input="$v.password.$touch"
/>
<woot-input
v-model="passwordConfirmation"
type="password"
:styles="inputStyles"
:class="{ error: $v.passwordConfirmation.$error }"
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL')"
:placeholder="
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
"
:error="`${
$v.passwordConfirmation.$error
? $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR')
: ''
}`"
@input="$v.passwordConfirmation.$touch"
/>
<form-button
type="submit"
color-scheme="primary"
variant="solid"
size="large"
:disabled="isButtonDisabled"
>
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
</form-button>
</div>
</form>
</template>
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import FormButton from 'v3/components/Form/Button.vue';
export default {
components: {
FormButton,
},
mixins: [alertMixin],
data() {
return {
currentPassword: '',
password: '',
passwordConfirmation: '',
isPasswordChanging: false,
errorMessage: '',
inputStyles: {
borderRadius: '12px',
padding: '6px 12px',
fontSize: '14px',
marginBottom: '2px',
},
};
},
validations: {
currentPassword: {
required,
},
password: {
minLength: minLength(6),
},
passwordConfirmation: {
minLength: minLength(6),
isEqPassword(value) {
if (value !== this.password) {
return false;
}
return true;
},
},
},
computed: {
isButtonDisabled() {
return (
!this.currentPassword ||
!this.passwordConfirmation ||
!this.$v.passwordConfirmation.isEqPassword
);
},
},
methods: {
async changePassword() {
this.$v.$touch();
if (this.$v.$invalid) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
return;
}
let alertMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS');
try {
await this.$store.dispatch('updateProfile', {
password: this.password,
password_confirmation: this.passwordConfirmation,
current_password: this.currentPassword,
});
} catch (error) {
alertMessage =
parseAPIErrorResponse(error) ||
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
} finally {
this.showAlert(alertMessage);
}
},
},
};
</script>
@@ -0,0 +1,64 @@
<template>
<div
class="flex flex-col gap-4 w-full h-fit sm:max-h-[220px] p-4 sm:max-w-[350px] rounded-md border border-solid border-ash-200"
:class="{
'border-primary-300 ': active,
}"
@click="$emit('click')"
>
<div class="flex flex-col gap-2 items-center w-full rounded-t-[5px]">
<div class="flex items-center justify-between w-full gap-1">
<div class="flex items-center text-base font-medium text-ash-900">
{{ title }}
</div>
<input
:checked="active"
type="radio"
:name="`hotkey-${title}`"
class="shadow cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-5 h-5 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[18px] checked:before:h-[18px] checked:border checked:border-primary-600"
/>
</div>
<span class="text-ash-900 text-sm line-clamp-2 leading-[1.4] text-start">
{{ description }}
</span>
</div>
<div>
<img
:src="lightImage"
:alt="`Light themed image for ${title}`"
class="block object-cover w-full dark:hidden"
/>
<img
:src="darkImage"
:alt="`Dark themed image for ${title}`"
class="hidden object-cover w-full dark:block"
/>
</div>
</div>
</template>
<script setup>
defineProps({
active: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
lightImage: {
type: String,
default: '',
},
darkImage: {
type: String,
default: '',
},
});
</script>
@@ -1,7 +1,7 @@
<template>
<div class="flex items-center w-full overflow-y-auto">
<div class="flex flex-col h-full p-5 pt-16 mx-auto my-0 font-inter">
<div class="flex flex-col gap-16 sm:max-w-[720px]">
<div class="flex flex-col gap-16 pb-8 sm:max-w-[720px]">
<div class="flex flex-col gap-6">
<h2 class="mt-4 text-2xl font-medium text-ash-900">
{{ $t('PROFILE_SETTINGS.TITLE') }}
@@ -33,6 +33,48 @@
@update-signature="updateSignature"
/>
</form-section>
<form-section
:header="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.TITLE')"
:description="$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.NOTE')"
>
<div
class="flex flex-col justify-between w-full gap-5 sm:gap-4 sm:flex-row"
>
<button
v-for="hotKey in hotKeys"
:key="hotKey.key"
class="px-0 reset-base"
>
<hot-key-card
:key="hotKey.title"
:title="hotKey.title"
:description="hotKey.description"
:light-image="hotKey.lightImage"
:dark-image="hotKey.darkImage"
:active="isEditorHotKeyEnabled(uiSettings, hotKey.key)"
@click="toggleHotKey(hotKey.key)"
/>
</button>
</div>
</form-section>
<form-section
:header="$t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE')"
>
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
</form-section>
<form-section
:header="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')
"
:description="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
"
>
<audio-notifications />
</form-section>
<form-section :header="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
<notification-preferences />
</form-section>
</div>
</div>
</div>
@@ -49,6 +91,10 @@ import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
import UserProfilePicture from './UserProfilePicture.vue';
import UserBasicDetails from './UserBasicDetails.vue';
import MessageSignature from './MessageSignature.vue';
import HotKeyCard from './HotKeyCard.vue';
import ChangePassword from './ChangePassword.vue';
import NotificationPreferences from './NotificationPreferences.vue';
import AudioNotifications from './AudioNotifications.vue';
import FormSection from 'dashboard/components/FormSection.vue';
export default {
@@ -57,6 +103,10 @@ export default {
FormSection,
UserProfilePicture,
UserBasicDetails,
HotKeyCard,
ChangePassword,
NotificationPreferences,
AudioNotifications,
},
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
data() {
@@ -67,6 +117,31 @@ export default {
displayName: '',
email: '',
messageSignature: '',
hotKeys: [
{
key: 'enter',
title: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.HEADING'
),
description: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.ENTER_KEY.CONTENT'
),
lightImage: '/assets/images/dashboard/profile/hot-key-enter.svg',
darkImage: '/assets/images/dashboard/profile/hot-key-enter-dark.svg',
},
{
key: 'cmd_enter',
title: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.HEADING'
),
description: this.$t(
'PROFILE_SETTINGS.FORM.SEND_MESSAGE.CARD.CMD_ENTER_KEY.CONTENT'
),
lightImage: '/assets/images/dashboard/profile/hot-key-ctrl-enter.svg',
darkImage:
'/assets/images/dashboard/profile/hot-key-ctrl-enter-dark.svg',
},
],
};
},
computed: {
@@ -156,6 +231,15 @@ export default {
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_FAILED'));
}
},
toggleHotKey(key) {
this.hotKeys = this.hotKeys.map(hotKey =>
hotKey.key === key ? { ...hotKey, active: !hotKey.active } : hotKey
);
this.updateUISettings({ editor_message_key: key });
this.showAlert(
this.$t('PROFILE_SETTINGS.FORM.SEND_MESSAGE.UPDATE_SUCCESS')
);
},
},
};
</script>
@@ -0,0 +1,53 @@
<template>
<div
class="flex items-start gap-2 px-0 text-sm tracking-[0.5] text-left rtl:text-right"
:class="`col-span-${span}`"
>
<input
v-model="localFlags"
class="mt-1 flex-shrink-0 border-ash-200 border checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
type="checkbox"
:value="localValue"
@input="handleInput"
/>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
value: {
type: String,
required: true,
},
span: {
type: Number,
default: 2,
},
selectedFlags: {
type: Array,
required: true,
},
type: {
type: String,
required: true,
},
});
const emit = defineEmits(['input']);
const localValue = ref(props.value);
const localFlags = ref(props.selectedFlags);
watch(
() => props.selectedFlags,
newValue => {
localFlags.value = newValue;
}
);
const handleInput = e => {
emit('input', props.type, e);
};
</script>
@@ -0,0 +1,284 @@
<template>
<div id="profile-settings-notifications" class="flex flex-col gap-6">
<!-- Layout for desktop devices -->
<div class="hidden sm:block">
<div
class="grid content-center h-12 grid-cols-12 gap-4 py-0 rounded-t-xl"
>
<table-header-cell
:span="7"
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPE_TITLE')}`"
>
<span class="text-sm font-normal normal-case text-ash-800">
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPE_TITLE') }}
</span>
</table-header-cell>
<table-header-cell
:span="2"
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.EMAIL')}`"
>
<span class="text-sm font-medium normal-case text-ash-900">
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.EMAIL') }}
</span>
</table-header-cell>
<table-header-cell
:span="3"
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.PUSH')}`"
>
<div class="flex items-center justify-between gap-1">
<span
class="text-sm font-medium normal-case text-ash-900 whitespace-nowrap"
>
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.PUSH') }}
</span>
<form-switch
:value="hasEnabledPushPermissions"
@input="onRequestPermissions"
/>
</div>
</table-header-cell>
</div>
<div
v-for="(notification, index) in filteredNotificationTypes"
:key="index"
>
<div
class="grid items-center content-center h-12 grid-cols-12 gap-4 py-0 rounded-t-xl"
>
<div
class="flex flex-row items-start gap-2 col-span-7 px-0 py-2 text-sm tracking-[0.5] rtl:text-right"
>
<span class="text-sm text-ash-900">
{{ $t(notification.label) }}
</span>
</div>
<div
v-for="(type, typeIndex) in ['email', 'push']"
:key="typeIndex"
class="flex items-start gap-2 px-0 text-sm tracking-[0.5] text-left rtl:text-right"
:class="`col-span-${type === 'push' ? 3 : 2}`"
>
<CheckBox
:value="`${type}_${notification.value}`"
:is-checked="
checkFlagStatus(type, notification.value, selectedPushFlags)
"
@update="id => handleInput(type, id)"
/>
</div>
</div>
</div>
</div>
<!-- Layout for mobile devices -->
<div class="flex flex-col gap-6 sm:hidden">
<span class="text-sm font-medium normal-case text-ash-900">
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
</span>
<div class="flex flex-col gap-4">
<div
v-for="(notification, index) in filteredNotificationTypes"
:key="index"
class="flex flex-row items-start gap-2"
>
<CheckBox
:id="`email_${notification.value}`"
:value="`email_${notification.value}`"
:is-checked="checkFlagStatus('email', notification.value)"
@update="handleEmailInput"
/>
<span class="text-sm text-ash-900">{{ $t(notification.label) }}</span>
</div>
</div>
<div class="flex items-center justify-start gap-2">
<span class="text-sm font-medium normal-case text-ash-900">
{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.TITLE') }}
</span>
<form-switch
:value="hasEnabledPushPermissions"
@input="onRequestPermissions"
/>
</div>
<div class="flex flex-col gap-4">
<div
v-for="(notification, index) in filteredNotificationTypes"
:key="index"
class="flex flex-row items-start gap-2"
>
<CheckBox
:id="`push_${notification.value}`"
:value="`push_${notification.value}`"
:is-checked="checkFlagStatus('push', notification.value)"
@update="handlePushInput"
/>
<span class="text-sm text-ash-900">{{ $t(notification.label) }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import configMixin from 'shared/mixins/configMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import TableHeaderCell from 'dashboard/components/widgets/TableHeaderCell.vue';
import CheckBox from 'v3/components/Form/CheckBox.vue';
import {
hasPushPermissions,
requestPushPermissions,
verifyServiceWorkerExistence,
} from 'dashboard/helper/pushHelper.js';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import FormSwitch from 'v3/components/Form/Switch.vue';
import { NOTIFICATION_TYPES } from './constants';
export default {
components: {
TableHeaderCell,
FormSwitch,
CheckBox,
},
mixins: [alertMixin, configMixin, uiSettingsMixin],
data() {
return {
selectedEmailFlags: [],
selectedPushFlags: [],
enableAudioAlerts: false,
hasEnabledPushPermissions: false,
notificationTypes: NOTIFICATION_TYPES,
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
pushFlags: 'userNotificationSettings/getSelectedPushFlags',
uiSettings: 'getUISettings',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
hasPushAPISupport() {
return !!('Notification' in window);
},
isSLAEnabled() {
return this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.SLA);
},
filteredNotificationTypes() {
return this.notificationTypes.filter(notification =>
this.isSLAEnabled
? true
: ![
'sla_missed_first_response',
'sla_missed_next_response',
'sla_missed_resolution',
].includes(notification.value)
);
},
},
watch: {
emailFlags(value) {
this.selectedEmailFlags = value;
},
pushFlags(value) {
this.selectedPushFlags = value;
},
},
mounted() {
if (hasPushPermissions()) {
this.getPushSubscription();
}
this.$store.dispatch('userNotificationSettings/get');
},
methods: {
checkFlagStatus(type, flagType) {
const selectedFlags =
type === 'email' ? this.selectedEmailFlags : this.selectedPushFlags;
return selectedFlags.includes(`${type}_${flagType}`);
},
onRegistrationSuccess() {
this.hasEnabledPushPermissions = true;
},
onRequestPermissions() {
requestPushPermissions({
onSuccess: this.onRegistrationSuccess,
});
},
getPushSubscription() {
verifyServiceWorkerExistence(registration =>
registration.pushManager
.getSubscription()
.then(subscription => {
if (!subscription) {
this.hasEnabledPushPermissions = false;
} else {
this.hasEnabledPushPermissions = true;
}
})
// eslint-disable-next-line no-console
.catch(error => console.log(error))
);
},
async updateNotificationSettings() {
try {
this.$store.dispatch('userNotificationSettings/update', {
selectedEmailFlags: this.selectedEmailFlags,
selectedPushFlags: this.selectedPushFlags,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
} catch (error) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR'));
}
},
handleAudioInput(e) {
this.enableAudioAlerts = e.target.value;
this.updateUISettings({
enable_audio_alerts: this.enableAudioAlerts,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioAlertConditions(e) {
let condition = e.target.value;
if (condition === 'tab_is_inactive') {
this.updateUISettings({
always_play_audio_alert: !e.target.checked,
});
} else if (condition === 'conversations_are_read') {
this.updateUISettings({
alert_if_unread_assigned_conversation_exist: e.target.checked,
});
}
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioToneChange(value) {
this.updateUISettings({ notification_tone: value });
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleInput(type, id) {
if (type === 'email') {
this.handleEmailInput(id);
} else {
this.handlePushInput(id);
}
},
handleEmailInput(id) {
this.selectedEmailFlags = this.toggleInput(this.selectedEmailFlags, id);
this.updateNotificationSettings();
},
handlePushInput(id) {
this.selectedPushFlags = this.toggleInput(this.selectedPushFlags, id);
this.updateNotificationSettings();
},
toggleInput(selected, current) {
if (selected.includes(current)) {
const newSelectedFlags = selected.filter(flag => flag !== current);
return newSelectedFlags;
}
return [...selected, current];
},
},
};
</script>
@@ -0,0 +1,52 @@
export const NOTIFICATION_TYPES = [
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_CREATED',
value: 'conversation_creation',
},
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_ASSIGNED',
value: 'conversation_assignment',
},
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_MENTION',
value: 'conversation_mention',
},
{
label:
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.ASSIGNED_CONVERSATION_NEW_MESSAGE',
value: 'assigned_conversation_new_message',
},
{
label:
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.PARTICIPATING_CONVERSATION_NEW_MESSAGE',
value: 'participating_conversation_new_message',
},
{
label:
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_FIRST_RESPONSE',
value: 'sla_missed_first_response',
},
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_NEXT_RESPONSE',
value: 'sla_missed_next_response',
},
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_RESOLUTION',
value: 'sla_missed_resolution',
},
];
export const ALERT_EVENTS = [
{
value: 'none',
label: 'none',
},
{
value: 'mine',
label: 'mine',
},
{
value: 'all',
label: 'all',
},
];
@@ -22,14 +22,11 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import SLAPopoverCard from 'dashboard/components/widgets/conversation/components/SLAPopoverCard.vue';
export default {
components: {
SLAPopoverCard,
},
mixins: [clickaway],
props: {
slaEvents: {
type: Array,
@@ -23,8 +23,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import SearchSuggestions from './SearchSuggestions.vue';
import PublicSearchInput from './PublicSearchInput.vue';
@@ -35,7 +33,6 @@ export default {
PublicSearchInput,
SearchSuggestions,
},
mixins: [clickaway],
props: {
value: {
type: [String, Number],
@@ -74,13 +74,11 @@
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
import { mixin as clickaway } from 'vue-clickaway';
export default {
components: {
Thumbnail,
MultiselectDropdownItems,
},
mixins: [clickaway],
props: {
options: {
type: Array,
@@ -11,22 +11,26 @@ export default {
if (events) {
const wrappedEvents = this.wrapEventsInKeybindingsHandler(events);
const keydownHandler = createKeybindingsHandler(wrappedEvents);
this.appendToHandler(keydownHandler);
document.addEventListener('keydown', keydownHandler);
this.addEventHandler(keydownHandler);
}
},
beforeDestroy() {
if (this.$el && this.$el.dataset.keydownHandlerIndex) {
if (this.$el && this.$el.dataset?.keydownHandlerIndex) {
const handlerToRemove =
taggedHandlers[this.$el.dataset.keydownHandlerIndex];
document.removeEventListener('keydown', handlerToRemove);
}
},
methods: {
appendToHandler(keydownHandler) {
addEventHandler(keydownHandler) {
const indexToAppend = taggedHandlers.push(keydownHandler) - 1;
const root = this.$el;
root.dataset.keydownHandlerIndex = indexToAppend;
if (root && root.dataset) {
// For the components with a top level v-if Vue renders it as an empty comment in the DOM
// so we need to check if the root element has a dataset property to ensure it is a valid element
document.addEventListener('keydown', keydownHandler);
root.dataset.keydownHandlerIndex = indexToAppend;
}
},
getKeyboardEvents() {
return null;
+2 -1
View File
@@ -58,7 +58,8 @@ const attrs = useAttrs();
const baseClasses = {
outline: 'outline outline-1 -outline-offset-1 focus:ring focus:ring-offset-1',
ghost: 'hover:text-600 active:text-600 focus:ring focus:ring-offset-1',
solid: 'hover:bg-700 active:bg-700 focus:ring focus:ring-offset-1',
solid:
'hover:bg-700 active:bg-700 focus:ring focus:ring-offset-1 focus:ring-2',
};
const colorClass = computed(() => {
@@ -0,0 +1,31 @@
<template>
<input
:id="value"
v-model="checked"
type="checkbox"
:value="value"
class="flex-shrink-0 mt-0.5 border-ash-200 border checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
/>
</template>
<script setup>
import { defineProps, defineEmits, computed } from 'vue';
const props = defineProps({
isChecked: {
type: Boolean,
default: false,
},
value: {
type: String,
default: null,
},
});
const emit = defineEmits(['input']);
const checked = computed({
get: () => props.isChecked,
set: value => emit('update', props.value, value),
});
</script>
+3 -3
View File
@@ -11,11 +11,11 @@
:selected="value"
:name="name"
:class="{
'text-slate-400': !value,
'text-slate-900 dark:text-slate-100': value,
'text-ash-400': !value,
'text-ash-900': value,
'pl-9': icon,
}"
class="block w-full px-3 py-2 pr-6 mb-0 border-0 rounded-md shadow-sm outline-none appearance-none select-caret ring-1 ring-inset placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-700 dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200"
class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm outline-none appearance-none rounded-xl select-caret ring-ash-200 ring-1 ring-inset placeholder:text-ash-900 focus:ring-2 focus:ring-inset focus:ring-primary-500 sm:text-sm sm:leading-6"
@input="onInput"
>
<option value="" disabled selected class="hidden">
@@ -0,0 +1,35 @@
<template>
<button
type="button"
class="relative flex-shrink-0 h-4 p-0 border-none shadow-inner w-7 rounded-3xl"
:class="
value ? 'bg-primary-600 shadow-primary-800' : 'shadow-ash-400 bg-ash-200'
"
role="switch"
:aria-checked="value.toString()"
@click="onClick"
>
<span
aria-hidden="true"
class="rounded-full bg-white top-0.5 absolute dark:bg-white w-3 h-3 translate-y-0 duration-200 transition-transform ease-in-out"
:class="
value
? 'ltr:translate-x-0 rtl:translate-x-[12px]'
: 'ltr:-translate-x-[12px] rtl:translate-x-0'
"
/>
</button>
</template>
<script>
export default {
props: {
value: { type: Boolean, default: false },
},
methods: {
onClick() {
this.$emit('input', !this.value);
},
},
};
</script>
@@ -49,7 +49,6 @@
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import ChatAttachmentButton from 'widget/components/ChatAttachment.vue';
import ChatSendButton from 'widget/components/ChatSendButton.vue';
@@ -69,7 +68,7 @@ export default {
FluentIcon,
ResizableTextArea,
},
mixins: [clickaway, configMixin, darkModeMixin],
mixins: [configMixin, darkModeMixin],
props: {
onSendMessage: {
type: Function,
@@ -83,7 +83,6 @@
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import countries from 'shared/constants/countries.js';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import mentionSelectionKeyboardMixin from 'dashboard/components/widgets/mentions/mentionSelectionKeyboardMixin.js';
@@ -94,12 +93,7 @@ export default {
components: {
FluentIcon,
},
mixins: [
mentionSelectionKeyboardMixin,
FormulateInputMixin,
darkModeMixin,
clickaway,
],
mixins: [mentionSelectionKeyboardMixin, FormulateInputMixin, darkModeMixin],
props: {
placeholder: {
type: String,