Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e90600c03 | ||
|
|
19410dfa3d |
@@ -77,7 +77,7 @@ gem 'jwt'
|
||||
gem 'pundit'
|
||||
# super admin
|
||||
gem 'administrate', '>= 0.20.1'
|
||||
gem 'administrate-field-active_storage', '>= 1.0.2'
|
||||
gem 'administrate-field-active_storage', '>= 1.0.1'
|
||||
gem 'administrate-field-belongs_to_search', '>= 0.9.0'
|
||||
|
||||
##--- gems for pubsub service ---##
|
||||
@@ -113,10 +113,10 @@ gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.14.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.15.0', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.14.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.2.4'
|
||||
gem 'sidekiq', '>= 7.2.1'
|
||||
# We want cron jobs
|
||||
gem 'sidekiq-cron', '>= 1.12.0'
|
||||
|
||||
|
||||
+15
-17
@@ -113,7 +113,7 @@ GEM
|
||||
kaminari (~> 1.2.2)
|
||||
sassc-rails (~> 2.1)
|
||||
selectize-rails (~> 0.6)
|
||||
administrate-field-active_storage (1.0.2)
|
||||
administrate-field-active_storage (1.0.1)
|
||||
administrate (>= 0.2.2)
|
||||
rails (>= 7.0)
|
||||
administrate-field-belongs_to_search (0.9.0)
|
||||
@@ -150,7 +150,6 @@ GEM
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.20)
|
||||
bigdecimal (3.1.7)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -448,13 +447,13 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
meta_request (0.8.2)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 8)
|
||||
method_source (1.1.0)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0218.1)
|
||||
@@ -474,7 +473,7 @@ GEM
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.10)
|
||||
net-imap (0.4.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -489,7 +488,7 @@ GEM
|
||||
sidekiq
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.1)
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.4)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
@@ -604,7 +603,7 @@ GEM
|
||||
ffi (~> 1.0)
|
||||
redis (5.0.6)
|
||||
redis-client (>= 0.9.0)
|
||||
redis-client (0.22.1)
|
||||
redis-client (0.19.1)
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
@@ -703,19 +702,18 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.17.3)
|
||||
sentry-rails (5.14.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-ruby (5.17.3)
|
||||
bigdecimal
|
||||
sentry-ruby (~> 5.14.0)
|
||||
sentry-ruby (5.14.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.17.3)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-sidekiq (5.14.0)
|
||||
sentry-ruby (~> 5.14.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.2.4)
|
||||
sidekiq (7.2.1)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
@@ -837,7 +835,7 @@ DEPENDENCIES
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate (>= 0.20.1)
|
||||
administrate-field-active_storage (>= 1.0.2)
|
||||
administrate-field-active_storage (>= 1.0.1)
|
||||
administrate-field-belongs_to_search (>= 0.9.0)
|
||||
annotate
|
||||
attr_extras
|
||||
@@ -933,9 +931,9 @@ DEPENDENCIES
|
||||
seed_dump
|
||||
sentry-rails (>= 5.14.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.15.0)
|
||||
sentry-sidekiq (>= 5.14.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.2.4)
|
||||
sidekiq (>= 7.2.1)
|
||||
sidekiq-cron (>= 1.12.0)
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client (~> 2.2.0)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span
|
||||
class="w-full inline-flex gap-1.5 items-start font-medium whitespace-nowrap text-sm mb-0"
|
||||
class="w-full font-medium text-sm mb-0"
|
||||
:class="
|
||||
$v.editedValue.$error
|
||||
? 'text-red-400 dark:text-red-500'
|
||||
@@ -20,11 +20,6 @@
|
||||
"
|
||||
>
|
||||
{{ label }}
|
||||
<helper-text-popup
|
||||
v-if="description"
|
||||
:message="description"
|
||||
class="mt-0.5"
|
||||
/>
|
||||
</span>
|
||||
<woot-button
|
||||
v-if="showCopyAndDeleteButton"
|
||||
@@ -46,7 +41,7 @@
|
||||
ref="inputfield"
|
||||
v-model="editedValue"
|
||||
:type="inputType"
|
||||
class="!h-8 ltr:!rounded-r-none rtl:!rounded-l-none !mb-0 !text-sm"
|
||||
class="!h-8 ltr:rounded-r-none rtl:rounded-l-none !mb-0 !text-sm"
|
||||
autofocus="true"
|
||||
:class="{ error: $v.editedValue.$error }"
|
||||
@blur="$v.editedValue.$touch"
|
||||
@@ -135,25 +130,22 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { required, url } from 'vuelidate/lib/validators';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue';
|
||||
import { isValidURL } from '../helper/URLHelper';
|
||||
import customAttributeMixin from '../mixins/customAttributeMixin';
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiselectDropdown,
|
||||
HelperTextPopup,
|
||||
},
|
||||
mixins: [customAttributeMixin],
|
||||
mixins: [customAttributeMixin, clickaway],
|
||||
props: {
|
||||
label: { type: String, required: true },
|
||||
description: { type: String, default: '' },
|
||||
values: { type: Array, default: () => [] },
|
||||
value: { type: [String, Number, Boolean], default: '' },
|
||||
showActions: { type: Boolean, default: false },
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-start w-full gap-6">
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<h4 v-if="title" class="text-lg font-medium text-ash-900">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex-grow h-px bg-ash-200" />
|
||||
</div>
|
||||
<p v-if="description" class="mb-0 text-sm font-normal text-ash-900">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-6">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -88,6 +88,7 @@
|
||||
<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';
|
||||
@@ -108,7 +109,7 @@ export default {
|
||||
WootDropdownMenu,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [alertMixin, keyboardEventListenerMixins],
|
||||
mixins: [clickaway, alertMixin, keyboardEventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -24,7 +24,6 @@ import SubmitButton from './buttons/FormSubmitButton';
|
||||
import Tabs from './ui/Tabs/Tabs';
|
||||
import TabsItem from './ui/Tabs/TabsItem';
|
||||
import Thumbnail from './widgets/Thumbnail.vue';
|
||||
import DatePicker from './ui/DatePicker/DatePicker.vue';
|
||||
|
||||
const WootUIKit = {
|
||||
AvatarUploader,
|
||||
@@ -52,7 +51,6 @@ const WootUIKit = {
|
||||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
DatePicker,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
|
||||
<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';
|
||||
@@ -66,7 +67,7 @@ export default {
|
||||
AvailabilityStatusBadge,
|
||||
},
|
||||
|
||||
mixins: [alertMixin],
|
||||
mixins: [clickaway, alertMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
</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';
|
||||
@@ -118,6 +119,7 @@ export default {
|
||||
WootDropdownItem,
|
||||
AvailabilityStatus,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import {
|
||||
getActiveDateRange,
|
||||
moveCalendarDate,
|
||||
DATE_RANGE_TYPES,
|
||||
CALENDAR_TYPES,
|
||||
CALENDAR_PERIODS,
|
||||
} from './helpers/DatePickerHelper';
|
||||
import {
|
||||
isValid,
|
||||
startOfMonth,
|
||||
subDays,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
isBefore,
|
||||
subMonths,
|
||||
addMonths,
|
||||
isSameMonth,
|
||||
differenceInCalendarMonths,
|
||||
setMonth,
|
||||
setYear,
|
||||
isAfter,
|
||||
} from 'date-fns';
|
||||
|
||||
import DatePickerButton from './components/DatePickerButton.vue';
|
||||
import CalendarDateInput from './components/CalendarDateInput.vue';
|
||||
import CalendarDateRange from './components/CalendarDateRange.vue';
|
||||
import CalendarYear from './components/CalendarYear.vue';
|
||||
import CalendarMonth from './components/CalendarMonth.vue';
|
||||
import CalendarWeek from './components/CalendarWeek.vue';
|
||||
import CalendarFooter from './components/CalendarFooter.vue';
|
||||
|
||||
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;
|
||||
|
||||
const showDatePicker = ref(false);
|
||||
const calendarViews = ref({ start: WEEK, end: WEEK });
|
||||
const currentDate = ref(new Date());
|
||||
const selectedStartDate = ref(startOfDay(subDays(currentDate.value, 6))); // LAST_7_DAYS
|
||||
const selectedEndDate = ref(endOfDay(currentDate.value));
|
||||
// Setting the start and end calendar
|
||||
const startCurrentDate = ref(startOfDay(selectedStartDate.value));
|
||||
const endCurrentDate = ref(
|
||||
isSameMonth(selectedStartDate.value, selectedEndDate.value)
|
||||
? startOfMonth(addMonths(selectedEndDate.value, 1)) // Moves to the start of the next month if dates are in the same month (Mounted case LAST_7_DAYS)
|
||||
: startOfMonth(selectedEndDate.value) // Always shows the month of the end date starting from the first (Mounted case LAST_7_DAYS)
|
||||
);
|
||||
const selectingEndDate = ref(false);
|
||||
const selectedRange = ref(LAST_7_DAYS);
|
||||
const hoveredEndDate = ref(null);
|
||||
|
||||
const manualStartDate = ref(selectedStartDate.value);
|
||||
const manualEndDate = ref(selectedEndDate.value);
|
||||
|
||||
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 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 isLastSevenOrThirtyDays =
|
||||
newRange === LAST_7_DAYS || newRange === LAST_30_DAYS;
|
||||
startCurrentDate.value = selectedStartDate.value;
|
||||
endCurrentDate.value =
|
||||
isLastSevenOrThirtyDays &&
|
||||
isSameMonth(selectedStartDate.value, selectedEndDate.value)
|
||||
? startOfMonth(addMonths(selectedStartDate.value, 1))
|
||||
: selectedEndDate.value;
|
||||
selectingEndDate.value = false;
|
||||
} else if (!selectingEndDate.value) {
|
||||
// If selecting a custom range and not selecting an end date, set the start date to the selected start date
|
||||
startCurrentDate.value = startOfDay(currentDate.value);
|
||||
}
|
||||
});
|
||||
|
||||
// Watcher will set the input values based on the selected start and end dates
|
||||
watch(
|
||||
[selectedStartDate, selectedEndDate],
|
||||
([newStart, newEnd]) => {
|
||||
if (isValid(newStart)) {
|
||||
manualStartDate.value = newStart;
|
||||
} else {
|
||||
manualStartDate.value = selectedStartDate.value;
|
||||
}
|
||||
|
||||
if (isValid(newEnd)) {
|
||||
manualEndDate.value = newEnd;
|
||||
} else {
|
||||
manualEndDate.value = selectedEndDate.value;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Watcher to ensure dates are always in logical order
|
||||
// This watch is will ensure that the start date is always before the end date
|
||||
watch(
|
||||
[startCurrentDate, endCurrentDate],
|
||||
([newStart, newEnd], [oldStart, oldEnd]) => {
|
||||
const monthDifference = differenceInCalendarMonths(newEnd, newStart);
|
||||
|
||||
if (newStart !== oldStart) {
|
||||
if (isAfter(newStart, newEnd) || monthDifference === 0) {
|
||||
// Adjust the end date forward if the start date is adjusted and is after the end date or in the same month
|
||||
endCurrentDate.value = addMonths(newStart, 1);
|
||||
}
|
||||
}
|
||||
if (newEnd !== oldEnd) {
|
||||
if (isBefore(newEnd, newStart) || monthDifference === 0) {
|
||||
// Adjust the start date backward if the end date is adjusted and is before the start date or in the same month
|
||||
startCurrentDate.value = subMonths(newEnd, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const setDateRange = range => {
|
||||
selectedRange.value = range.value;
|
||||
const { start, end } = getActiveDateRange(range.value, currentDate.value);
|
||||
selectedStartDate.value = start;
|
||||
selectedEndDate.value = end;
|
||||
};
|
||||
|
||||
const moveCalendar = (calendar, direction, period = MONTH) => {
|
||||
const { start, end } = moveCalendarDate(
|
||||
calendar,
|
||||
startCurrentDate.value,
|
||||
endCurrentDate.value,
|
||||
direction,
|
||||
period
|
||||
);
|
||||
startCurrentDate.value = start;
|
||||
endCurrentDate.value = end;
|
||||
};
|
||||
|
||||
const selectDate = day => {
|
||||
selectedRange.value = CUSTOM_RANGE;
|
||||
if (!selectingEndDate.value || day < selectedStartDate.value) {
|
||||
selectedStartDate.value = day;
|
||||
selectedEndDate.value = null;
|
||||
selectingEndDate.value = true;
|
||||
} else {
|
||||
selectedEndDate.value = day;
|
||||
selectingEndDate.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setViewMode = (calendar, mode) => {
|
||||
selectedRange.value = CUSTOM_RANGE;
|
||||
calendarViews.value[calendar] = mode;
|
||||
};
|
||||
|
||||
const openCalendar = (index, calendarType, period = MONTH) => {
|
||||
const current =
|
||||
calendarType === START_CALENDAR
|
||||
? startCurrentDate.value
|
||||
: endCurrentDate.value;
|
||||
const newDate =
|
||||
period === MONTH
|
||||
? setMonth(startOfMonth(current), index)
|
||||
: setYear(current, index);
|
||||
if (calendarType === START_CALENDAR) {
|
||||
startCurrentDate.value = newDate;
|
||||
} else {
|
||||
endCurrentDate.value = newDate;
|
||||
}
|
||||
setViewMode(calendarType, period === MONTH ? WEEK : MONTH);
|
||||
};
|
||||
|
||||
const updateManualInput = (newDate, calendarType) => {
|
||||
if (calendarType === START_CALENDAR) {
|
||||
selectedStartDate.value = newDate;
|
||||
startCurrentDate.value = newDate;
|
||||
} else {
|
||||
selectedEndDate.value = newDate;
|
||||
endCurrentDate.value = newDate;
|
||||
}
|
||||
selectingEndDate.value = false;
|
||||
};
|
||||
|
||||
const handleManualInputError = message => {
|
||||
bus.$emit('newToastMessage', message);
|
||||
};
|
||||
|
||||
const resetDatePicker = () => {
|
||||
startCurrentDate.value = startOfDay(currentDate.value); // Resets to today at start of the day
|
||||
endCurrentDate.value = addMonths(startOfDay(currentDate.value), 1); // Resets to one month ahead
|
||||
selectedStartDate.value = startOfDay(subDays(currentDate.value, 6));
|
||||
selectedEndDate.value = endOfDay(currentDate.value);
|
||||
selectingEndDate.value = false;
|
||||
selectedRange.value = LAST_7_DAYS;
|
||||
// Reset view modes if they are being used to toggle between different calendar views
|
||||
calendarViews.value = { start: WEEK, end: WEEK };
|
||||
};
|
||||
|
||||
const emitDateRange = () => {
|
||||
if (!isValid(selectedStartDate.value) || !isValid(selectedEndDate.value)) {
|
||||
bus.$emit('newToastMessage', 'Please select a valid time range');
|
||||
} else {
|
||||
showDatePicker.value = false;
|
||||
emit('dateRangeChanged', [selectedStartDate.value, selectedEndDate.value]);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative font-inter">
|
||||
<DatePickerButton
|
||||
:selected-start-date="selectedStartDate"
|
||||
:selected-end-date="selectedEndDate"
|
||||
:selected-range="selectedRange"
|
||||
@open="showDatePicker = !showDatePicker"
|
||||
/>
|
||||
<div
|
||||
v-if="showDatePicker"
|
||||
class="flex absolute top-9 ltr:left-0 rtl:right-0 z-30 shadow-md select-none w-[880px] h-[490px] rounded-2xl border border-slate-50 dark:border-slate-800 bg-white dark:bg-slate-800"
|
||||
>
|
||||
<CalendarDateRange
|
||||
:selected-range="selectedRange"
|
||||
@set-range="setDateRange"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50"
|
||||
>
|
||||
<div class="flex justify-around h-fit">
|
||||
<!-- Calendars for Start and End Dates -->
|
||||
<div
|
||||
v-for="calendar in [START_CALENDAR, END_CALENDAR]"
|
||||
:key="`${calendar}-calendar`"
|
||||
class="flex flex-col items-center"
|
||||
>
|
||||
<CalendarDateInput
|
||||
:calendar-type="calendar"
|
||||
:date-value="
|
||||
calendar === START_CALENDAR ? manualStartDate : manualEndDate
|
||||
"
|
||||
:compare-date="
|
||||
calendar === START_CALENDAR ? manualEndDate : manualStartDate
|
||||
"
|
||||
:is-disabled="selectedRange !== CUSTOM_RANGE"
|
||||
@update="
|
||||
calendar === START_CALENDAR
|
||||
? (manualStartDate = $event)
|
||||
: (manualEndDate = $event)
|
||||
"
|
||||
@validate="updateManualInput($event, calendar)"
|
||||
@error="handleManualInputError($event)"
|
||||
/>
|
||||
<div class="py-5 border-b border-slate-50 dark:border-slate-700/50">
|
||||
<div
|
||||
class="flex flex-col items-center gap-2 px-5 min-w-[340px] max-h-[352px]"
|
||||
:class="
|
||||
calendar === START_CALENDAR &&
|
||||
'ltr:border-r rtl:border-l border-slate-50 dark:border-slate-700/50'
|
||||
"
|
||||
>
|
||||
<CalendarYear
|
||||
v-if="calendarViews[calendar] === YEAR"
|
||||
:calendar-type="calendar"
|
||||
:start-current-date="startCurrentDate"
|
||||
:end-current-date="endCurrentDate"
|
||||
@select-year="openCalendar($event, calendar, YEAR)"
|
||||
/>
|
||||
<CalendarMonth
|
||||
v-else-if="calendarViews[calendar] === MONTH"
|
||||
:calendar-type="calendar"
|
||||
:start-current-date="startCurrentDate"
|
||||
:end-current-date="endCurrentDate"
|
||||
@select-month="openCalendar($event, calendar)"
|
||||
@set-view="setViewMode"
|
||||
@prev="moveCalendar(calendar, 'prev', YEAR)"
|
||||
@next="moveCalendar(calendar, 'next', YEAR)"
|
||||
/>
|
||||
<CalendarWeek
|
||||
v-else-if="calendarViews[calendar] === WEEK"
|
||||
:calendar-type="calendar"
|
||||
:current-date="currentDate"
|
||||
:start-current-date="startCurrentDate"
|
||||
:end-current-date="endCurrentDate"
|
||||
:selected-start-date="selectedStartDate"
|
||||
:selected-end-date="selectedEndDate"
|
||||
:selecting-end-date="selectingEndDate"
|
||||
:hovered-end-date="hoveredEndDate"
|
||||
@update-hovered-end-date="hoveredEndDate = $event"
|
||||
@select-date="selectDate"
|
||||
@set-view="setViewMode"
|
||||
@prev="moveCalendar(calendar, 'prev')"
|
||||
@next="moveCalendar(calendar, 'next')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CalendarFooter @change="emitDateRange" @clear="resetDatePicker" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script setup>
|
||||
import { CALENDAR_PERIODS } from '../helpers/DatePickerHelper';
|
||||
|
||||
defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
},
|
||||
firstButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
viewMode: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['prev', 'next', 'set-view']);
|
||||
|
||||
const { YEAR } = CALENDAR_PERIODS;
|
||||
|
||||
const onClickPrev = type => {
|
||||
emit('prev', type);
|
||||
};
|
||||
|
||||
const onClickNext = type => {
|
||||
emit('next', type);
|
||||
};
|
||||
|
||||
const onClickSetView = (type, mode) => {
|
||||
emit('set-view', type, mode);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-start justify-between w-full h-9">
|
||||
<button
|
||||
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180"
|
||||
@click="onClickPrev(calendarType)"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="chevron-left"
|
||||
size="14"
|
||||
class="text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</button>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
v-if="firstButtonLabel"
|
||||
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50 hover:text-woot-600 dark:hover:text-woot-600"
|
||||
@click="onClickSetView(calendarType, viewMode)"
|
||||
>
|
||||
{{ firstButtonLabel }}
|
||||
</button>
|
||||
<button
|
||||
v-if="buttonLabel"
|
||||
class="p-0 text-sm font-medium text-center text-slate-800 dark:text-slate-50"
|
||||
:class="{ 'hover:text-woot-600 dark:hover:text-woot-600': viewMode }"
|
||||
@click="onClickSetView(calendarType, YEAR)"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 rounded-lg hover:bg-slate-75 dark:hover:bg-slate-700/50 rtl:rotate-180"
|
||||
@click="onClickNext(calendarType)"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="chevron-right"
|
||||
size="14"
|
||||
class="text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,74 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { parse, isValid, isAfter, isBefore } from 'date-fns';
|
||||
import {
|
||||
getIntlDateFormatForLocale,
|
||||
CALENDAR_TYPES,
|
||||
} from '../helpers/DatePickerHelper';
|
||||
|
||||
const props = defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
dateValue: Date,
|
||||
compareDate: Date,
|
||||
isDisabled: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update', 'validate', 'error']);
|
||||
|
||||
const { START_CALENDAR, END_CALENDAR } = CALENDAR_TYPES;
|
||||
|
||||
const dateFormat = computed(() => getIntlDateFormatForLocale()?.toUpperCase());
|
||||
|
||||
const localDateValue = computed({
|
||||
get: () => props.dateValue?.toLocaleDateString(navigator.language) || '',
|
||||
set: newValue => {
|
||||
const format = getIntlDateFormatForLocale();
|
||||
const parsedDate = parse(newValue, format, new Date());
|
||||
if (isValid(parsedDate)) {
|
||||
emit('update', parsedDate);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const validateDate = () => {
|
||||
if (!isValid(props.dateValue)) {
|
||||
emit('error', `Please enter the date in valid format: ${dateFormat.value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { calendarType, compareDate, dateValue } = props;
|
||||
const isStartCalendar = calendarType === START_CALENDAR;
|
||||
const isEndCalendar = calendarType === END_CALENDAR;
|
||||
|
||||
if (compareDate && isStartCalendar && isAfter(dateValue, compareDate)) {
|
||||
emit('error', 'Start date must be before the end date.');
|
||||
} else if (compareDate && isEndCalendar && isBefore(dateValue, compareDate)) {
|
||||
emit('error', 'End date must be after the start date.');
|
||||
} else {
|
||||
emit('validate', dateValue);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[82px] flex flex-col items-start px-5 gap-1.5 pt-4 w-full">
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-50">
|
||||
{{
|
||||
calendarType === START_CALENDAR
|
||||
? $t('DATE_PICKER.DATE_RANGE_INPUT.START')
|
||||
: $t('DATE_PICKER.DATE_RANGE_INPUT.END')
|
||||
}}
|
||||
</span>
|
||||
<input
|
||||
v-model="localDateValue"
|
||||
type="text"
|
||||
class="reset-base border bg-slate-25 dark:bg-slate-900 ring-offset-ash-900 border-slate-50 dark:border-slate-700/50 w-full disabled:text-slate-200 dark:disabled:text-slate-700 disabled:cursor-not-allowed text-slate-800 dark:text-slate-50 px-1.5 py-1 text-sm rounded-xl h-10"
|
||||
:placeholder="dateFormat"
|
||||
:disabled="isDisabled"
|
||||
@keypress.enter="validateDate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,41 +0,0 @@
|
||||
<script setup>
|
||||
import { dateRanges } from '../helpers/DatePickerHelper';
|
||||
|
||||
defineProps({
|
||||
selectedRange: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['set-range']);
|
||||
|
||||
const setDateRange = range => {
|
||||
emit('set-range', range);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-[200px] flex flex-col items-start">
|
||||
<h4
|
||||
class="w-full px-5 py-4 text-sm font-medium capitalize text-start text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('DATE_PICKER.DATE_RANGE_OPTIONS.TITLE') }}
|
||||
</h4>
|
||||
<div class="flex flex-col items-start w-full">
|
||||
<button
|
||||
v-for="range in dateRanges"
|
||||
:key="range.label"
|
||||
class="w-full px-5 py-3 text-sm font-medium truncate border-none rounded-none text-start hover:bg-slate-50 dark:hover:bg-slate-700"
|
||||
:class="
|
||||
range.value === selectedRange
|
||||
? 'text-slate-800 dark:text-slate-50 bg-slate-50 dark:bg-slate-700'
|
||||
: 'text-slate-600 dark:text-slate-200'
|
||||
"
|
||||
@click="setDateRange(range)"
|
||||
>
|
||||
{{ $t(range.label) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script setup>
|
||||
const emit = defineEmits(['clear', 'apply']);
|
||||
|
||||
const onClickClear = () => {
|
||||
emit('clear');
|
||||
};
|
||||
|
||||
const onClickApply = () => {
|
||||
emit('change');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[56px] flex justify-between px-5 py-3 items-center">
|
||||
<button
|
||||
class="p-1.5 rounded-lg w-fit text-sm font-medium text-slate-600 dark:text-slate-200 hover:text-slate-800 dark:hover:text-slate-100"
|
||||
@click="onClickClear"
|
||||
>
|
||||
{{ $t('DATE_PICKER.CLEAR_BUTTON') }}
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 rounded-lg w-fit text-sm font-medium text-woot-500 dark:text-woot-300 hover:text-woot-700 dark:hover:text-woot-500"
|
||||
@click="onClickApply"
|
||||
>
|
||||
{{ $t('DATE_PICKER.APPLY_BUTTON') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { format, getMonth, setMonth, startOfMonth } from 'date-fns';
|
||||
import {
|
||||
yearName,
|
||||
CALENDAR_TYPES,
|
||||
CALENDAR_PERIODS,
|
||||
} from '../helpers/DatePickerHelper';
|
||||
|
||||
import CalendarAction from './CalendarAction.vue';
|
||||
|
||||
const props = defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
},
|
||||
startCurrentDate: Date,
|
||||
endCurrentDate: Date,
|
||||
});
|
||||
|
||||
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||
const { MONTH, YEAR } = CALENDAR_PERIODS;
|
||||
|
||||
const months = Array.from({ length: 12 }, (_, index) =>
|
||||
format(setMonth(startOfMonth(new Date()), index), 'MMM')
|
||||
);
|
||||
|
||||
const activeMonthIndex = computed(() => {
|
||||
const date =
|
||||
props.calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate;
|
||||
return getMonth(date);
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select-month', 'prev', 'next', 'set-view']);
|
||||
|
||||
const setViewMode = (type, mode) => {
|
||||
emit('set-view', type, mode);
|
||||
};
|
||||
|
||||
const onClickPrev = () => {
|
||||
emit('prev');
|
||||
};
|
||||
|
||||
const onClickNext = () => {
|
||||
emit('next');
|
||||
};
|
||||
|
||||
const selectMonth = index => {
|
||||
emit('select-month', index);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||
<CalendarAction
|
||||
:view-mode="YEAR"
|
||||
:calendar-type="calendarType"
|
||||
:button-label="
|
||||
yearName(
|
||||
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate,
|
||||
MONTH
|
||||
)
|
||||
"
|
||||
@set-view="setViewMode"
|
||||
@prev="onClickPrev"
|
||||
@next="onClickNext"
|
||||
/>
|
||||
|
||||
<div class="grid w-full grid-cols-3 gap-x-3 gap-y-2 auto-rows-[61px]">
|
||||
<button
|
||||
v-for="(month, index) in months"
|
||||
:key="index"
|
||||
class="p-2 text-sm font-medium text-center text-slate-800 dark:text-slate-50 w-[92px] h-10 rounded-lg py-2.5 px-2 hover:bg-slate-75 dark:hover:bg-slate-700"
|
||||
:class="{
|
||||
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white hover:bg-woot-500 dark:bg-woot-700':
|
||||
index === activeMonthIndex,
|
||||
}"
|
||||
@click="selectMonth(index)"
|
||||
>
|
||||
{{ month }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,172 +0,0 @@
|
||||
<script setup>
|
||||
import {
|
||||
monthName,
|
||||
yearName,
|
||||
getWeeksForMonth,
|
||||
isToday,
|
||||
dayIsInRange,
|
||||
isCurrentMonth,
|
||||
isLastDayOfMonth,
|
||||
isHoveringDayInRange,
|
||||
isHoveringNextDayInRange,
|
||||
CALENDAR_TYPES,
|
||||
CALENDAR_PERIODS,
|
||||
} from '../helpers/DatePickerHelper';
|
||||
|
||||
import CalendarWeekLabel from './CalendarWeekLabel.vue';
|
||||
import CalendarAction from './CalendarAction.vue';
|
||||
|
||||
const props = defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
},
|
||||
currentDate: Date,
|
||||
startCurrentDate: Date,
|
||||
endCurrentDate: Date,
|
||||
selectedStartDate: Date,
|
||||
selectingEndDate: Boolean,
|
||||
selectedEndDate: Date,
|
||||
hoveredEndDate: Date,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'update-hovered-end-date',
|
||||
'select-date',
|
||||
'prev',
|
||||
'next',
|
||||
'set-view',
|
||||
]);
|
||||
|
||||
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||
const { MONTH } = CALENDAR_PERIODS;
|
||||
|
||||
const emitHoveredEndDate = day => {
|
||||
emit('update-hovered-end-date', day);
|
||||
};
|
||||
|
||||
const emitSelectDate = day => {
|
||||
emit('select-date', day);
|
||||
};
|
||||
const onClickPrev = () => {
|
||||
emit('prev');
|
||||
};
|
||||
|
||||
const onClickNext = () => {
|
||||
emit('next');
|
||||
};
|
||||
|
||||
const setViewMode = (type, mode) => {
|
||||
emit('set-view', type, mode);
|
||||
};
|
||||
|
||||
const weeks = calendarType => {
|
||||
return getWeeksForMonth(
|
||||
calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate
|
||||
);
|
||||
};
|
||||
|
||||
const isSelectedStartOrEndDate = day => {
|
||||
return (
|
||||
dayIsInRange(day, props.selectedStartDate, props.selectedStartDate) ||
|
||||
dayIsInRange(day, props.selectedEndDate, props.selectedEndDate)
|
||||
);
|
||||
};
|
||||
|
||||
const isInRange = day => {
|
||||
return dayIsInRange(day, props.selectedStartDate, props.selectedEndDate);
|
||||
};
|
||||
|
||||
const isInCurrentMonth = day => {
|
||||
return isCurrentMonth(
|
||||
day,
|
||||
props.calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate
|
||||
);
|
||||
};
|
||||
|
||||
const isHoveringInRange = day => {
|
||||
return isHoveringDayInRange(
|
||||
day,
|
||||
props.selectedStartDate,
|
||||
props.selectingEndDate,
|
||||
props.hoveredEndDate
|
||||
);
|
||||
};
|
||||
|
||||
const isNextDayInRange = day => {
|
||||
return isHoveringNextDayInRange(
|
||||
day,
|
||||
props.selectedStartDate,
|
||||
props.selectedEndDate,
|
||||
props.hoveredEndDate
|
||||
);
|
||||
};
|
||||
|
||||
const dayClasses = day => ({
|
||||
'text-slate-500 dark:text-slate-400 pointer-events-none':
|
||||
!isInCurrentMonth(day),
|
||||
'text-slate-800 dark:text-slate-50 hover:text-slate-800 dark:hover:text-white hover:bg-woot-100 dark:hover:bg-woot-700':
|
||||
isInCurrentMonth(day),
|
||||
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white':
|
||||
isSelectedStartOrEndDate(day) && isInCurrentMonth(day),
|
||||
'bg-woot-50 dark:bg-woot-800':
|
||||
(isInRange(day) || isHoveringInRange(day)) &&
|
||||
!isSelectedStartOrEndDate(day) &&
|
||||
isInCurrentMonth(day),
|
||||
'outline outline-1 outline-woot-200 -outline-offset-1 dark:outline-woot-700 text-woot-600 dark:text-woot-400':
|
||||
isToday(props.currentDate, day) && !isSelectedStartOrEndDate(day),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||
<CalendarAction
|
||||
:view-mode="MONTH"
|
||||
:calendar-type="calendarType"
|
||||
:first-button-label="
|
||||
monthName(
|
||||
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate
|
||||
)
|
||||
"
|
||||
:button-label="
|
||||
yearName(
|
||||
calendarType === START_CALENDAR ? startCurrentDate : endCurrentDate
|
||||
)
|
||||
"
|
||||
@prev="onClickPrev"
|
||||
@next="onClickNext"
|
||||
@set-view="setViewMode"
|
||||
/>
|
||||
<CalendarWeekLabel />
|
||||
<div
|
||||
v-for="week in weeks(calendarType)"
|
||||
:key="week[0].getTime()"
|
||||
class="grid max-w-md grid-cols-7 gap-2 mx-auto overflow-hidden rounded-lg"
|
||||
>
|
||||
<div
|
||||
v-for="day in week"
|
||||
:key="day.getTime()"
|
||||
class="flex relative items-center justify-center w-9 h-8 py-1.5 px-2 font-medium text-sm rounded-lg cursor-pointer"
|
||||
:class="dayClasses(day)"
|
||||
@mouseenter="emitHoveredEndDate(day)"
|
||||
@mouseleave="emitHoveredEndDate(null)"
|
||||
@click="emitSelectDate(day)"
|
||||
>
|
||||
{{ day.getDate() }}
|
||||
<span
|
||||
v-if="
|
||||
(isInRange(day) || isHoveringInRange(day)) &&
|
||||
isNextDayInRange(day) &&
|
||||
!isLastDayOfMonth(day) &&
|
||||
isInCurrentMonth(day)
|
||||
"
|
||||
class="absolute bottom-0 w-6 h-8 ltr:-right-4 rtl:-left-4 bg-woot-50 dark:bg-woot-800 -z-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup>
|
||||
import { calendarWeeks } from '../helpers/DatePickerHelper';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-md mx-auto grid grid-cols-7 gap-2">
|
||||
<div
|
||||
v-for="day in calendarWeeks"
|
||||
:key="day.id"
|
||||
class="flex items-center justify-center font-medium text-sm w-9 h-7 py-1.5 px-2"
|
||||
>
|
||||
{{ day.label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { getYear, addYears, subYears } from 'date-fns';
|
||||
import { CALENDAR_TYPES } from '../helpers/DatePickerHelper';
|
||||
|
||||
import CalendarAction from './CalendarAction.vue';
|
||||
|
||||
const props = defineProps({
|
||||
calendarType: {
|
||||
type: String,
|
||||
default: 'start',
|
||||
},
|
||||
startCurrentDate: Date,
|
||||
endCurrentDate: Date,
|
||||
});
|
||||
|
||||
const { START_CALENDAR } = CALENDAR_TYPES;
|
||||
|
||||
const calculateStartYear = date => {
|
||||
const year = getYear(date);
|
||||
return year - (year % 10); // Align with the beginning of a decade
|
||||
};
|
||||
|
||||
const startYear = ref(
|
||||
calculateStartYear(
|
||||
props.calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate
|
||||
)
|
||||
);
|
||||
|
||||
const years = computed(() =>
|
||||
Array.from({ length: 10 }, (_, i) => startYear.value + i)
|
||||
);
|
||||
|
||||
const firstYear = computed(() => years.value[0]);
|
||||
const lastYear = computed(() => years.value[years.value.length - 1]);
|
||||
|
||||
const activeYear = computed(() => {
|
||||
const date =
|
||||
props.calendarType === START_CALENDAR
|
||||
? props.startCurrentDate
|
||||
: props.endCurrentDate;
|
||||
return getYear(date);
|
||||
});
|
||||
|
||||
const onClickPrev = () => {
|
||||
startYear.value = subYears(new Date(startYear.value, 0, 1), 10).getFullYear();
|
||||
};
|
||||
|
||||
const onClickNext = () => {
|
||||
startYear.value = addYears(new Date(startYear.value, 0, 1), 10).getFullYear();
|
||||
};
|
||||
|
||||
const emit = defineEmits(['select-year']);
|
||||
|
||||
const selectYear = year => {
|
||||
emit('select-year', year);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-2 max-h-[312px]">
|
||||
<CalendarAction
|
||||
:calendar-type="calendarType"
|
||||
:button-label="`${firstYear} - ${lastYear}`"
|
||||
@prev="onClickPrev"
|
||||
@next="onClickNext"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-3 gap-y-2 w-full auto-rows-[47px]">
|
||||
<button
|
||||
v-for="year in years"
|
||||
:key="year"
|
||||
class="p-2 text-sm font-medium text-center text-slate-800 dark:text-slate-50 w-[144px] h-10 rounded-lg py-2.5 px-2 hover:bg-slate-75 dark:hover:bg-slate-700"
|
||||
:class="{
|
||||
'bg-woot-600 dark:bg-woot-600 text-white dark:text-white hover:bg-woot-500 dark:hover:bg-woot-700':
|
||||
year === activeYear,
|
||||
}"
|
||||
@click="selectYear(year)"
|
||||
>
|
||||
{{ year }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,71 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { dateRanges } from '../helpers/DatePickerHelper';
|
||||
import { format, isSameYear, isValid } from 'date-fns';
|
||||
|
||||
const props = defineProps({
|
||||
selectedStartDate: Date,
|
||||
selectedEndDate: Date,
|
||||
selectedRange: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const formatDateRange = computed(() => {
|
||||
const startDate = props.selectedStartDate;
|
||||
const endDate = props.selectedEndDate;
|
||||
|
||||
if (!isValid(startDate) || !isValid(endDate)) {
|
||||
return 'Select a date range';
|
||||
}
|
||||
|
||||
const formatString = isSameYear(startDate, endDate)
|
||||
? 'MMM d' // Same year: "Apr 1"
|
||||
: 'MMM d yyyy'; // Different years: "Apr 1 2025"
|
||||
|
||||
if (isSameYear(startDate, new Date()) && isSameYear(endDate, new Date())) {
|
||||
// Both dates are in the current year
|
||||
return `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}`;
|
||||
}
|
||||
// At least one date is not in the current year
|
||||
return `${format(startDate, formatString)} - ${format(
|
||||
endDate,
|
||||
formatString
|
||||
)}`;
|
||||
});
|
||||
|
||||
const activeDateRange = computed(
|
||||
() => dateRanges.find(range => range.value === props.selectedRange).label
|
||||
);
|
||||
|
||||
const emit = defineEmits(['open']);
|
||||
|
||||
const openDatePicker = () => {
|
||||
emit('open');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex relative items-center rounded-lg gap-2 py-1.5 px-3 h-8 bg-slate-50 dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
|
||||
@click="openDatePicker"
|
||||
>
|
||||
<fluent-icon
|
||||
class="text-slate-800 dark:text-slate-50"
|
||||
icon="calendar"
|
||||
size="16"
|
||||
/>
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-50">
|
||||
{{ $t(activeDateRange) }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-slate-600 dark:text-slate-200">
|
||||
{{ formatDateRange }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
class="text-slate-800 dark:text-slate-50"
|
||||
icon="chevron-down"
|
||||
size="14"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
@@ -1,219 +0,0 @@
|
||||
import {
|
||||
startOfDay,
|
||||
subDays,
|
||||
endOfDay,
|
||||
subMonths,
|
||||
addMonths,
|
||||
subYears,
|
||||
addYears,
|
||||
startOfMonth,
|
||||
isSameMonth,
|
||||
format,
|
||||
startOfWeek,
|
||||
addDays,
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
isSameDay,
|
||||
isWithinInterval,
|
||||
} from 'date-fns';
|
||||
|
||||
// Constants for calendar and date ranges
|
||||
export const calendarWeeks = [
|
||||
{ id: 1, label: 'M' },
|
||||
{ id: 2, label: 'T' },
|
||||
{ id: 3, label: 'W' },
|
||||
{ id: 4, label: 'T' },
|
||||
{ id: 5, label: 'F' },
|
||||
{ id: 6, label: 'S' },
|
||||
{ id: 7, label: 'S' },
|
||||
];
|
||||
|
||||
export const dateRanges = [
|
||||
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_7_DAYS', value: 'last7days' },
|
||||
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_30_DAYS', value: 'last30days' },
|
||||
{
|
||||
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_3_MONTHS',
|
||||
value: 'last3months',
|
||||
},
|
||||
{
|
||||
label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_6_MONTHS',
|
||||
value: 'last6months',
|
||||
},
|
||||
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.LAST_YEAR', value: 'lastYear' },
|
||||
{ label: 'DATE_PICKER.DATE_RANGE_OPTIONS.CUSTOM_RANGE', value: 'custom' },
|
||||
];
|
||||
|
||||
export const DATE_RANGE_TYPES = {
|
||||
LAST_7_DAYS: 'last7days',
|
||||
LAST_30_DAYS: 'last30days',
|
||||
LAST_3_MONTHS: 'last3months',
|
||||
LAST_6_MONTHS: 'last6months',
|
||||
LAST_YEAR: 'lastYear',
|
||||
CUSTOM_RANGE: 'custom',
|
||||
};
|
||||
|
||||
export const CALENDAR_TYPES = {
|
||||
START_CALENDAR: 'start',
|
||||
END_CALENDAR: 'end',
|
||||
};
|
||||
|
||||
export const CALENDAR_PERIODS = {
|
||||
WEEK: 'week',
|
||||
MONTH: 'month',
|
||||
YEAR: 'year',
|
||||
};
|
||||
|
||||
// Utility functions for date operations
|
||||
export const monthName = currentDate => format(currentDate, 'MMMM');
|
||||
export const yearName = currentDate => format(currentDate, 'yyyy');
|
||||
|
||||
export const getIntlDateFormatForLocale = () => {
|
||||
const year = 2222;
|
||||
const month = 12;
|
||||
const day = 15;
|
||||
const date = new Date(year, month - 1, day);
|
||||
const formattedDate = new Intl.DateTimeFormat(navigator.language).format(
|
||||
date
|
||||
);
|
||||
return formattedDate
|
||||
.replace(`${year}`, 'yyyy')
|
||||
.replace(`${month}`, 'MM')
|
||||
.replace(`${day}`, 'dd');
|
||||
};
|
||||
|
||||
// Utility functions for calendar operations
|
||||
export const chunk = (array, size) =>
|
||||
Array.from({ length: Math.ceil(array.length / size) }, (_, index) =>
|
||||
array.slice(index * size, index * size + size)
|
||||
);
|
||||
|
||||
export const getWeeksForMonth = (date, weekStartsOn = 1) => {
|
||||
const startOfTheMonth = startOfMonth(date);
|
||||
const startOfTheFirstWeek = startOfWeek(startOfTheMonth, { weekStartsOn });
|
||||
const endOfTheLastWeek = addDays(startOfTheFirstWeek, 41); // Covering six weeks to fill the calendar
|
||||
return chunk(
|
||||
eachDayOfInterval({ start: startOfTheFirstWeek, end: endOfTheLastWeek }),
|
||||
7
|
||||
);
|
||||
};
|
||||
|
||||
export const moveCalendarDate = (
|
||||
calendar,
|
||||
startCurrentDate,
|
||||
endCurrentDate,
|
||||
direction,
|
||||
period
|
||||
) => {
|
||||
const adjustFunctions = {
|
||||
month: { prev: subMonths, next: addMonths },
|
||||
year: { prev: subYears, next: addYears },
|
||||
};
|
||||
|
||||
const adjust = adjustFunctions[period][direction];
|
||||
|
||||
if (calendar === 'start') {
|
||||
const newStart = adjust(startCurrentDate, 1);
|
||||
return { start: newStart, end: endCurrentDate };
|
||||
}
|
||||
const newEnd = adjust(endCurrentDate, 1);
|
||||
return { start: startCurrentDate, end: newEnd };
|
||||
};
|
||||
|
||||
// Date comparison functions
|
||||
export const isToday = (currentDate, date) =>
|
||||
date.getDate() === currentDate.getDate() &&
|
||||
date.getMonth() === currentDate.getMonth() &&
|
||||
date.getFullYear() === currentDate.getFullYear();
|
||||
|
||||
export const isCurrentMonth = (day, referenceDate) =>
|
||||
isSameMonth(day, referenceDate);
|
||||
|
||||
export const isLastDayOfMonth = day => {
|
||||
const lastDay = endOfMonth(day);
|
||||
return day.getDate() === lastDay.getDate();
|
||||
};
|
||||
|
||||
export const dayIsInRange = (date, startDate, endDate) => {
|
||||
if (!startDate || !endDate) {
|
||||
return false;
|
||||
}
|
||||
// Normalize dates to ignore time differences
|
||||
let startOfDayStart = startOfDay(startDate);
|
||||
let startOfDayEnd = startOfDay(endDate);
|
||||
// Swap if start is greater than end
|
||||
if (startOfDayStart > startOfDayEnd) {
|
||||
[startOfDayStart, startOfDayEnd] = [startOfDayEnd, startOfDayStart];
|
||||
}
|
||||
// Check if the date is within the interval or is the same as the start date
|
||||
return (
|
||||
isSameDay(date, startOfDayStart) ||
|
||||
isWithinInterval(date, {
|
||||
start: startOfDayStart,
|
||||
end: startOfDayEnd,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Handling hovering states in date range pickers
|
||||
export const isHoveringDayInRange = (
|
||||
day,
|
||||
startDate,
|
||||
endDate,
|
||||
hoveredEndDate
|
||||
) => {
|
||||
if (endDate && hoveredEndDate && startDate <= hoveredEndDate) {
|
||||
// Ensure the start date is not after the hovered end date
|
||||
return isWithinInterval(day, { start: startDate, end: hoveredEndDate });
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isHoveringNextDayInRange = (
|
||||
day,
|
||||
startDate,
|
||||
endDate,
|
||||
hoveredEndDate
|
||||
) => {
|
||||
if (startDate && !endDate && hoveredEndDate) {
|
||||
// If a start date is selected, and we're hovering (but haven't clicked an end date yet)
|
||||
const nextDay = addDays(day, 1);
|
||||
return isWithinInterval(nextDay, { start: startDate, end: hoveredEndDate });
|
||||
}
|
||||
if (startDate && endDate) {
|
||||
// Normal range checking between selected start and end dates
|
||||
const nextDay = addDays(day, 1);
|
||||
return isWithinInterval(nextDay, { start: startDate, end: endDate });
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Helper func to determine active date ranges based on user selection
|
||||
export const getActiveDateRange = (range, currentDate) => {
|
||||
const ranges = {
|
||||
last7days: () => ({
|
||||
start: startOfDay(subDays(currentDate, 6)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
last30days: () => ({
|
||||
start: startOfDay(subDays(currentDate, 29)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
last3months: () => ({
|
||||
start: startOfDay(subMonths(currentDate, 3)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
last6months: () => ({
|
||||
start: startOfDay(subMonths(currentDate, 6)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
lastYear: () => ({
|
||||
start: startOfDay(subMonths(currentDate, 12)),
|
||||
end: endOfDay(currentDate),
|
||||
}),
|
||||
custom: () => ({ start: currentDate, end: currentDate }),
|
||||
};
|
||||
|
||||
return (
|
||||
ranges[range] || (() => ({ start: currentDate, end: currentDate }))
|
||||
)();
|
||||
};
|
||||
-309
@@ -1,309 +0,0 @@
|
||||
import {
|
||||
monthName,
|
||||
yearName,
|
||||
getIntlDateFormatForLocale,
|
||||
getWeeksForMonth,
|
||||
isToday,
|
||||
isCurrentMonth,
|
||||
isLastDayOfMonth,
|
||||
dayIsInRange,
|
||||
getActiveDateRange,
|
||||
isHoveringDayInRange,
|
||||
isHoveringNextDayInRange,
|
||||
moveCalendarDate,
|
||||
chunk,
|
||||
} from '../DatePickerHelper';
|
||||
|
||||
describe('Date formatting functions', () => {
|
||||
const testDate = new Date(2020, 4, 15); // May 15, 2020
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns the correct month name from a date', () => {
|
||||
expect(monthName(testDate)).toBe('May');
|
||||
});
|
||||
|
||||
it('returns the correct year from a date', () => {
|
||||
expect(yearName(testDate)).toBe('2020');
|
||||
});
|
||||
|
||||
it('returns the correct date format for the current locale en-US', () => {
|
||||
const expected = 'MM/dd/yyyy';
|
||||
expect(getIntlDateFormatForLocale()).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns the correct date format for the current locale en-IN', () => {
|
||||
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
|
||||
const expected = 'dd/MM/yyyy';
|
||||
expect(getIntlDateFormatForLocale()).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunk', () => {
|
||||
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
it('correctly chunks an array into smaller arrays of given size', () => {
|
||||
const expected = [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8, 9],
|
||||
];
|
||||
expect(chunk(array, 3)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles arrays that do not divide evenly by the chunk size', () => {
|
||||
const expected = [[1, 2], [3, 4], [5, 6], [7, 8], [9]];
|
||||
expect(chunk(array, 2)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWeeksForMonth', () => {
|
||||
it('returns the correct weeks array for a month starting on Monday', () => {
|
||||
const date = new Date(2020, 3, 1); // April 2020
|
||||
const weeks = getWeeksForMonth(date, 1);
|
||||
expect(weeks.length).toBe(6);
|
||||
expect(weeks[0][0]).toEqual(new Date(2020, 2, 30)); // Check if first day of the first week is correct
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveCalendarDate', () => {
|
||||
it('handles the year transition when moving the start date backward by one month from January', () => {
|
||||
const startDate = new Date(2021, 0, 1);
|
||||
const endDate = new Date(2021, 0, 31);
|
||||
const result = moveCalendarDate(
|
||||
'start',
|
||||
startDate,
|
||||
endDate,
|
||||
'prev',
|
||||
'month'
|
||||
);
|
||||
const expectedStartDate = new Date(2020, 11, 1);
|
||||
expect(result.start.toISOString()).toEqual(expectedStartDate.toISOString());
|
||||
expect(result.end.toISOString()).toEqual(endDate.toISOString());
|
||||
});
|
||||
|
||||
it('handles the year transition when moving the start date forward by one month from January', () => {
|
||||
const startDate = new Date(2020, 0, 1);
|
||||
const endDate = new Date(2020, 1, 31);
|
||||
const result = moveCalendarDate(
|
||||
'start',
|
||||
startDate,
|
||||
endDate,
|
||||
'next',
|
||||
'month'
|
||||
);
|
||||
const expectedStartDate = new Date(2020, 1, 1);
|
||||
expect(result.start.toISOString()).toEqual(expectedStartDate.toISOString());
|
||||
expect(result.end.toISOString()).toEqual(endDate.toISOString());
|
||||
});
|
||||
|
||||
it('handles the year transition when moving the end date forward by one month from December', () => {
|
||||
const startDate = new Date(2021, 11, 1);
|
||||
const endDate = new Date(2021, 11, 31);
|
||||
const result = moveCalendarDate('end', startDate, endDate, 'next', 'month');
|
||||
const expectedEndDate = new Date(2022, 0, 31);
|
||||
expect(result.start).toEqual(startDate);
|
||||
expect(result.end.toISOString()).toEqual(expectedEndDate.toISOString());
|
||||
});
|
||||
it('handles the year transition when moving the end date backward by one month from December', () => {
|
||||
const startDate = new Date(2021, 11, 1);
|
||||
const endDate = new Date(2021, 11, 31);
|
||||
const result = moveCalendarDate('end', startDate, endDate, 'prev', 'month');
|
||||
const expectedEndDate = new Date(2021, 10, 30);
|
||||
|
||||
expect(result.start).toEqual(startDate);
|
||||
expect(result.end.toISOString()).toEqual(expectedEndDate.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToday', () => {
|
||||
it('returns true if the dates are the same', () => {
|
||||
const today = new Date();
|
||||
const alsoToday = new Date(today);
|
||||
expect(isToday(today, alsoToday)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the dates are not the same', () => {
|
||||
const today = new Date();
|
||||
const notToday = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() - 1
|
||||
);
|
||||
expect(isToday(today, notToday)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCurrentMonth', () => {
|
||||
const referenceDate = new Date(2020, 6, 15); // July 15, 2020
|
||||
|
||||
it('returns true if the day is in the same month as the reference date', () => {
|
||||
const testDay = new Date(2020, 6, 1);
|
||||
expect(isCurrentMonth(testDay, referenceDate)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the day is not in the same month as the reference date', () => {
|
||||
const testDay = new Date(2020, 5, 30);
|
||||
expect(isCurrentMonth(testDay, referenceDate)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLastDayOfMonth', () => {
|
||||
it('returns true if the day is the last day of the month', () => {
|
||||
const testDay = new Date(2020, 6, 31); // July 31, 2020
|
||||
expect(isLastDayOfMonth(testDay)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the day is not the last day of the month', () => {
|
||||
const testDay = new Date(2020, 6, 30); // July 30, 2020
|
||||
expect(isLastDayOfMonth(testDay)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dayIsInRange', () => {
|
||||
it('returns true if the date is within the range', () => {
|
||||
const start = new Date(2020, 1, 10);
|
||||
const end = new Date(2020, 1, 20);
|
||||
const testDate = new Date(2020, 1, 15);
|
||||
expect(dayIsInRange(testDate, start, end)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true if the date is the same as the start date', () => {
|
||||
const start = new Date(2020, 1, 10);
|
||||
const end = new Date(2020, 1, 20);
|
||||
const testDate = new Date(2020, 1, 10);
|
||||
expect(dayIsInRange(testDate, start, end)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the date is outside the range', () => {
|
||||
const start = new Date(2020, 1, 10);
|
||||
const end = new Date(2020, 1, 20);
|
||||
const testDate = new Date(2020, 1, 9);
|
||||
expect(dayIsInRange(testDate, start, end)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHoveringDayInRange', () => {
|
||||
const startDate = new Date(2020, 6, 10);
|
||||
const endDate = new Date(2020, 6, 20);
|
||||
const hoveredEndDate = new Date(2020, 6, 15);
|
||||
|
||||
it('returns true if the day is within the start and hovered end dates', () => {
|
||||
const testDay = new Date(2020, 6, 12);
|
||||
expect(
|
||||
isHoveringDayInRange(testDay, startDate, endDate, hoveredEndDate)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the day is outside the hovered date range', () => {
|
||||
const testDay = new Date(2020, 6, 16);
|
||||
expect(
|
||||
isHoveringDayInRange(testDay, startDate, endDate, hoveredEndDate)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHoveringNextDayInRange', () => {
|
||||
const startDate = new Date(2020, 6, 10);
|
||||
const hoveredEndDate = new Date(2020, 6, 15);
|
||||
|
||||
it('returns true if the next day after the given day is within the start and hovered end dates', () => {
|
||||
const testDay = new Date(2020, 6, 14);
|
||||
expect(
|
||||
isHoveringNextDayInRange(testDay, startDate, null, hoveredEndDate)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if the next day is outside the start and hovered end dates', () => {
|
||||
const testDay = new Date(2020, 6, 15);
|
||||
expect(
|
||||
isHoveringNextDayInRange(testDay, startDate, null, hoveredEndDate)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveDateRange', () => {
|
||||
const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon
|
||||
|
||||
beforeAll(() => {
|
||||
// Mocking the current date to ensure consistency in tests
|
||||
jest.useFakeTimers().setSystemTime(currentDate.getTime());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns the correct range for "last7days"', () => {
|
||||
const range = getActiveDateRange('last7days', new Date());
|
||||
const expectedStart = new Date(2020, 5, 9);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "last30days"', () => {
|
||||
const range = getActiveDateRange('last30days', new Date());
|
||||
const expectedStart = new Date(2020, 4, 17);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "last3months"', () => {
|
||||
const range = getActiveDateRange('last3months', new Date());
|
||||
const expectedStart = new Date(2020, 2, 15);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "last6months"', () => {
|
||||
const range = getActiveDateRange('last6months', new Date());
|
||||
const expectedStart = new Date(2019, 11, 15);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "lastYear"', () => {
|
||||
const range = getActiveDateRange('lastYear', new Date());
|
||||
const expectedStart = new Date(2019, 5, 15);
|
||||
expectedStart.setHours(0, 0, 0, 0);
|
||||
const expectedEnd = new Date();
|
||||
expectedEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(range.start).toEqual(expectedStart);
|
||||
expect(range.end).toEqual(expectedEnd);
|
||||
});
|
||||
|
||||
it('returns the correct range for "custom date range"', () => {
|
||||
const range = getActiveDateRange('custom', new Date());
|
||||
expect(range.start).toEqual(new Date(currentDate));
|
||||
expect(range.end).toEqual(new Date(currentDate));
|
||||
});
|
||||
|
||||
it('handles an unknown range label gracefully', () => {
|
||||
const range = getActiveDateRange('unknown', new Date());
|
||||
expect(range.start).toEqual(new Date(currentDate));
|
||||
expect(range.end).toEqual(new Date(currentDate));
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative group w-[inherit] whitespace-normal z-20">
|
||||
<fluent-icon
|
||||
icon="info"
|
||||
size="14"
|
||||
class="mt-0.5 text-slate-600 absolute dark:text-slate-400"
|
||||
/>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 w-fit ltr:left-4 rtl:right-4 top-0 border p-2.5 group-hover:flex items-center hidden absolute border-slate-75 dark:border-slate-800 rounded-lg shadow-md"
|
||||
>
|
||||
<p class="text-slate-800 dark:text-slate-75 mb-0 text-xs">
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,11 +18,13 @@
|
||||
|
||||
<script>
|
||||
import { Chrome } from 'vue-color';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Chrome,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
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 {
|
||||
@@ -41,7 +42,7 @@ export default {
|
||||
LabelDropdown,
|
||||
},
|
||||
|
||||
mixins: [adminMixin, keyboardEventListenerMixins],
|
||||
mixins: [clickaway, adminMixin, keyboardEventListenerMixins],
|
||||
|
||||
props: {
|
||||
allLabels: {
|
||||
|
||||
+2
-1
@@ -45,6 +45,7 @@
|
||||
<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';
|
||||
|
||||
@@ -52,7 +53,7 @@ export default {
|
||||
components: {
|
||||
FilterItem,
|
||||
},
|
||||
mixins: [uiSettingsMixin],
|
||||
mixins: [clickaway, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
showActionsDropdown: false,
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</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';
|
||||
@@ -51,7 +52,7 @@ export default {
|
||||
EmailTranscriptModal,
|
||||
ResolveAction,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
mixins: [alertMixin, clickaway],
|
||||
data() {
|
||||
return {
|
||||
showEmailActionsModal: false,
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
@@ -217,6 +218,7 @@ export default {
|
||||
ArticleSearchPopover,
|
||||
},
|
||||
mixins: [
|
||||
clickaway,
|
||||
inboxMixin,
|
||||
uiSettingsMixin,
|
||||
alertMixin,
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
</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';
|
||||
@@ -199,7 +200,7 @@ export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [keyboardEventListenerMixins, timeMixin],
|
||||
mixins: [keyboardEventListenerMixins, clickaway, timeMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<script>
|
||||
import { evaluateSLAStatus } from '../helpers/SLAHelper';
|
||||
import SLAPopoverCard from './SLAPopoverCard.vue';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
const REFRESH_INTERVAL = 60000;
|
||||
|
||||
@@ -58,6 +59,7 @@ export default {
|
||||
components: {
|
||||
SLAPopoverCard,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
|
||||
+2
@@ -101,12 +101,14 @@
|
||||
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,
|
||||
|
||||
+2
@@ -74,9 +74,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
|
||||
+2
@@ -59,8 +59,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
|
||||
+2
@@ -36,6 +36,7 @@
|
||||
</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';
|
||||
|
||||
@@ -44,6 +45,7 @@ export default {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
selectedInboxes: {
|
||||
type: Array,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative phone-input--wrap">
|
||||
<div class="phone-input--wrap relative">
|
||||
<div class="phone-input" :class="{ 'has-error': error }">
|
||||
<div
|
||||
class="cursor-pointer py-2 pr-1.5 pl-2 rounded-tl-md rounded-bl-md flex items-center justify-center gap-1.5 bg-slate-25 dark:bg-slate-700 h-10 w-14"
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<span
|
||||
v-if="activeDialCode"
|
||||
class="flex py-2 pl-2 pr-0 text-base font-normal font-medium leading-normal bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100"
|
||||
class="flex bg-white dark:bg-slate-900 font-medium text-slate-800 dark:text-slate-100 font-normal text-base leading-normal py-2 pl-2 pr-0"
|
||||
>
|
||||
{{ activeDialCode }}
|
||||
</span>
|
||||
@@ -49,20 +49,20 @@
|
||||
}"
|
||||
@click="onSelectCountry(country)"
|
||||
>
|
||||
<span class="mr-1 text-base">{{ country.emoji }}</span>
|
||||
<span class="text-base mr-1">{{ country.emoji }}</span>
|
||||
|
||||
<span
|
||||
class="max-w-[7.5rem] overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{{ country.name }}
|
||||
</span>
|
||||
<span class="ml-1 text-xs text-slate-300 dark:text-slate-300">{{
|
||||
<span class="ml-1 text-slate-300 dark:text-slate-300 text-xs">{{
|
||||
country.dial_code
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="filteredCountriesBySearch.length === 0">
|
||||
<span
|
||||
class="flex items-center justify-center mt-4 text-sm text-slate-500 dark:text-slate-300"
|
||||
class="flex items-center justify-center text-sm text-slate-500 dark:text-slate-300 mt-4"
|
||||
>
|
||||
No results found
|
||||
</span>
|
||||
@@ -229,7 +229,6 @@ export default {
|
||||
ArrowUp: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
console.log('ArrowUp in PhoneInput');
|
||||
this.moveUp();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
@@ -237,7 +236,6 @@ export default {
|
||||
ArrowDown: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
console.log('ArrowDown in PhoneInput');
|
||||
this.moveDown();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
@@ -245,7 +243,6 @@ export default {
|
||||
Enter: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
console.log('Enter in PhoneInput');
|
||||
this.onSelectCountry(
|
||||
this.filteredCountriesBySearch[this.selectedIndex]
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ export default {
|
||||
ArrowUp: {
|
||||
action: e => {
|
||||
this.moveSelectionUp();
|
||||
console.log('ArrowUp in mentionSelectionKeyboardMixin');
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
@@ -39,7 +38,6 @@ export default {
|
||||
ArrowDown: {
|
||||
action: e => {
|
||||
this.moveSelectionDown();
|
||||
console.log('ArrowDown in mentionSelectionKeyboardMixin');
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
@@ -54,7 +52,6 @@ export default {
|
||||
Enter: {
|
||||
action: e => {
|
||||
this.onSelect();
|
||||
console.log('Enter in mentionSelectionKeyboardMixin');
|
||||
e.preventDefault();
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { SHORTCUT_KEYS } from './constants';
|
||||
import Hotkey from 'dashboard/components/base/Hotkey.vue';
|
||||
|
||||
@@ -94,6 +95,7 @@ export default {
|
||||
components: {
|
||||
Hotkey,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { getCurrentInstance } from 'vue';
|
||||
|
||||
export const useTrack = () => {
|
||||
const vm = getCurrentInstance();
|
||||
if (!vm) throw new Error('must be called in setup');
|
||||
|
||||
return vm.proxy.$track;
|
||||
};
|
||||
|
||||
export function useAlert(message, action) {
|
||||
bus.$emit('newToastMessage', message, action);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { getCurrentInstance, reactive, watchEffect } from 'vue';
|
||||
|
||||
/**
|
||||
* Returns the current route location. Equivalent to using `$route` inside
|
||||
* templates.
|
||||
*/
|
||||
export function useRoute() {
|
||||
const instance = getCurrentInstance();
|
||||
const route = reactive(Object.assign({}, instance.proxy.$root.$route));
|
||||
watchEffect(() => {
|
||||
Object.assign(route, instance.proxy.$root.$route);
|
||||
});
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the router instance. Equivalent to using `$router` inside
|
||||
* templates.
|
||||
*/
|
||||
export function useRouter() {
|
||||
const instance = getCurrentInstance();
|
||||
const router = instance.proxy.$root.$router;
|
||||
watchEffect(() => {
|
||||
if (router) {
|
||||
Object.assign(router, instance.proxy.$root.$router);
|
||||
}
|
||||
});
|
||||
return router;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { computed } from 'vue';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
|
||||
export const useStore = () => {
|
||||
const vm = getCurrentInstance();
|
||||
if (!vm) throw new Error('must be called in setup');
|
||||
return vm.proxy.$store;
|
||||
};
|
||||
|
||||
export const useStoreGetters = () => {
|
||||
const store = useStore();
|
||||
return Object.fromEntries(
|
||||
Object.keys(store.getters).map(getter => [
|
||||
getter,
|
||||
computed(() => store.getters[getter]),
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
export const mapGetter = key => {
|
||||
const store = useStore();
|
||||
return computed(() => store.getters[key]);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"DATE_PICKER": {
|
||||
"APPLY_BUTTON": "Apply",
|
||||
"CLEAR_BUTTON": "Clear",
|
||||
"DATE_RANGE_INPUT": {
|
||||
"START": "Start Date",
|
||||
"END": "End Date"
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
"TITLE": "DATE RANGE",
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_3_MONTHS": "Last 3 months",
|
||||
"LAST_6_MONTHS": "Last 6 months",
|
||||
"LAST_YEAR": "Last year",
|
||||
"CUSTOM_RANGE": "Custom date range"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
"body": "Add agents to the created inbox."
|
||||
},
|
||||
{
|
||||
"title": "Voilà!",
|
||||
"title": "Voila!",
|
||||
"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 for your Inbox",
|
||||
"PICK_NAME": "Pick A Name Your Inbox",
|
||||
"PICK_A_VALUE": "Pick a value"
|
||||
},
|
||||
"TWITTER": {
|
||||
@@ -62,7 +62,7 @@
|
||||
},
|
||||
"CHANNEL_WEBHOOK_URL": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Please enter your Webhook URL",
|
||||
"PLACEHOLDER": "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 Bandwidth API Key",
|
||||
"PLACEHOLDER": "Please enter your Bandwith API Key",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"API_SECRET": {
|
||||
"LABEL": "API Secret",
|
||||
"PLACEHOLDER": "Please enter your Bandwidth API Secret",
|
||||
"PLACEHOLDER": "Please enter your Bandwith 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 receive callbacks on events.",
|
||||
"SUBTITLE": "Configure the URL where you want to recieve callbacks on events.",
|
||||
"PLACEHOLDER": "Webhook URL"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create API Channel",
|
||||
@@ -279,7 +279,7 @@
|
||||
},
|
||||
"EMAIL_CHANNEL": {
|
||||
"TITLE": "Email Channel",
|
||||
"DESC": "Integrate your email inbox.",
|
||||
"DESC": "Integrate you 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 at least one agent to your new Inbox",
|
||||
"VALIDATION_ERROR": "Add atleast 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 your customer when they receive emails from your agents.",
|
||||
"SUB_TEXT": "Select the name shown to the 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 new API key to be 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_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 Verification Token",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verify 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 receive email",
|
||||
"TOGGLE_HELP": "Enabling IMAP will help the user to recieve email",
|
||||
"EDIT": {
|
||||
"SUCCESS_MESSAGE": "IMAP settings updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update IMAP settings"
|
||||
|
||||
@@ -32,7 +32,6 @@ import whatsappTemplates from './whatsappTemplates.json';
|
||||
import sla from './sla.json';
|
||||
import inbox from './inbox.json';
|
||||
import general from './general.json';
|
||||
import datePicker from './datePicker.json';
|
||||
|
||||
export default {
|
||||
...advancedFilters,
|
||||
@@ -69,5 +68,4 @@ export default {
|
||||
...whatsappTemplates,
|
||||
...inbox,
|
||||
...general,
|
||||
...datePicker,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"PASSWORD_UPDATE_SUCCESS": "Your password has been changed successfully",
|
||||
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
|
||||
"FORM": {
|
||||
"PICTURE": "Profile Picture",
|
||||
"AVATAR": "Profile Image",
|
||||
"ERROR": "Please fix form errors",
|
||||
"REMOVE_IMAGE": "Remove",
|
||||
@@ -57,19 +56,13 @@
|
||||
},
|
||||
"ACCESS_TOKEN": {
|
||||
"TITLE": "Access Token",
|
||||
"NOTE": "This token can be used if you are building an API based integration",
|
||||
"COPY": "Copy"
|
||||
"NOTE": "This token can be used if you are building an API based integration"
|
||||
},
|
||||
"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 for conversations:",
|
||||
"TITLE": "Alert events:",
|
||||
"NONE": "None",
|
||||
"ASSIGNED": "Assigned Conversations",
|
||||
"ALL_CONVERSATIONS": "All Conversations"
|
||||
@@ -95,22 +88,6 @@
|
||||
"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,6 +106,7 @@
|
||||
<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';
|
||||
@@ -123,7 +124,7 @@ export default {
|
||||
TranslateModal,
|
||||
MenuItem,
|
||||
},
|
||||
mixins: [alertMixin, messageFormatterMixin],
|
||||
mixins: [alertMixin, clickaway, messageFormatterMixin],
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
|
||||
@@ -73,6 +73,7 @@ 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 {
|
||||
@@ -83,6 +84,7 @@ export default {
|
||||
SearchResultConversationsList,
|
||||
SearchResultMessagesList,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 'all',
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { VeTable } from 'vue-easytable';
|
||||
import { getCountryFlag } from 'dashboard/helper/flag';
|
||||
|
||||
@@ -44,7 +45,7 @@ export default {
|
||||
Spinner,
|
||||
VeTable,
|
||||
},
|
||||
mixins: [timeMixin, rtlMixin],
|
||||
mixins: [clickaway, timeMixin, rtlMixin],
|
||||
props: {
|
||||
contacts: {
|
||||
type: Array,
|
||||
|
||||
@@ -23,6 +23,15 @@
|
||||
:key="element.name"
|
||||
class="bg-white dark:bg-gray-800"
|
||||
>
|
||||
<div v-if="element.name === 'linear'" class="conversation--actions">
|
||||
<accordion-item
|
||||
title="Linear"
|
||||
:is-open="isContactSidebarItemOpen('is_linear_open')"
|
||||
@click="value => toggleSidebarUIState('is_linear_open', value)"
|
||||
>
|
||||
<linear :conversation-id="conversationId" />
|
||||
</accordion-item>
|
||||
</div>
|
||||
<div
|
||||
v-if="element.name === 'conversation_actions'"
|
||||
class="conversation--actions"
|
||||
@@ -145,6 +154,7 @@ import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||
import draggable from 'vuedraggable';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import MacrosList from './Macros/List.vue';
|
||||
import Linear from './linear/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -157,6 +167,7 @@ export default {
|
||||
ConversationParticipant,
|
||||
draggable,
|
||||
MacrosList,
|
||||
Linear,
|
||||
},
|
||||
mixins: [alertMixin, uiSettingsMixin],
|
||||
props: {
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
</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';
|
||||
@@ -91,7 +92,7 @@ export default {
|
||||
ThumbnailGroup,
|
||||
MultiselectDropdownItems,
|
||||
},
|
||||
mixins: [alertMixin, agentMixin],
|
||||
mixins: [alertMixin, agentMixin, clickaway],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
<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';
|
||||
|
||||
@@ -41,7 +42,7 @@ export default {
|
||||
components: {
|
||||
MacroPreview,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
mixins: [alertMixin, clickaway],
|
||||
props: {
|
||||
macro: {
|
||||
type: Object,
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
</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';
|
||||
@@ -194,7 +195,7 @@ export default {
|
||||
NewConversation,
|
||||
ContactMergeModal,
|
||||
},
|
||||
mixins: [alertMixin, adminMixin, timeMixin],
|
||||
mixins: [alertMixin, adminMixin, clickaway, timeMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
|
||||
-1
@@ -7,7 +7,6 @@
|
||||
:attribute-type="attribute.attribute_display_type"
|
||||
:values="attribute.attribute_values"
|
||||
:label="attribute.attribute_display_name"
|
||||
:description="attribute.attribute_description"
|
||||
:value="attribute.value"
|
||||
:show-actions="true"
|
||||
:attribute-regex="attribute.regex_pattern"
|
||||
|
||||
@@ -48,6 +48,7 @@ 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';
|
||||
@@ -59,7 +60,12 @@ export default {
|
||||
AddLabel,
|
||||
},
|
||||
|
||||
mixins: [conversationLabelMixin, adminMixin, keyboardEventListenerMixins],
|
||||
mixins: [
|
||||
clickaway,
|
||||
conversationLabelMixin,
|
||||
adminMixin,
|
||||
keyboardEventListenerMixins,
|
||||
],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: Number,
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
header-title="Create/link linear issue"
|
||||
header-content="Quickly create/link a linear issue from this conversation"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<div class="flex flex-col px-8 pb-4" @submit.prevent="onSubmit">
|
||||
<woot-input
|
||||
v-model="title"
|
||||
label="Title"
|
||||
class="w-full"
|
||||
placeholder="Enter title"
|
||||
type="text"
|
||||
@blur="$v.title.$touch"
|
||||
/>
|
||||
<label>
|
||||
Description
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="6"
|
||||
type="text"
|
||||
placeholder="Enter description"
|
||||
@blur="$v.description.$touch"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Team
|
||||
<select v-model="teamId" @change="onChangeTeam($event)">
|
||||
<option v-for="item in teams" :key="item.name" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Assignee
|
||||
<select v-model="assigneeId">
|
||||
<option v-for="item in assignees" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Priority
|
||||
<select v-model="priority">
|
||||
<option v-for="item in priorities" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Label
|
||||
<select v-model="labelId">
|
||||
<option v-for="item in labels" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Status
|
||||
<select v-model="stateId">
|
||||
<option v-for="item in statuses" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="flex items-center justify-end w-full gap-2 mt-8">
|
||||
<woot-button
|
||||
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
|
||||
@click.prevent="onClose"
|
||||
>
|
||||
{{ $t('SLA.FORM.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-disabled="isSubmitDisabled"
|
||||
class="px-4 rounded-xl"
|
||||
:is-loading="isCreating"
|
||||
@click.prevent="createIssue"
|
||||
>
|
||||
Create
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
|
||||
import { LinearClient } from '@linear/sdk';
|
||||
|
||||
// const chatwootAPIKey = '';
|
||||
const localAPIKey = '';
|
||||
const linearClient = new LinearClient({
|
||||
apiKey: localAPIKey,
|
||||
});
|
||||
// const BASE_URL = 'https:app.chatwoot.com';
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
|
||||
import validations from './validations';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
accountId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
teamId: '',
|
||||
assigneeId: '',
|
||||
stateId: '',
|
||||
labelId: '',
|
||||
priority: 0,
|
||||
teams: [],
|
||||
assignees: [],
|
||||
labels: [],
|
||||
statuses: [],
|
||||
searchTerm: '',
|
||||
priorities: [
|
||||
{
|
||||
id: 0,
|
||||
name: 'No priority',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Urgent',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'High',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Normal',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Low',
|
||||
},
|
||||
],
|
||||
isCreating: false,
|
||||
searchResults: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Test',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
validations,
|
||||
computed: {
|
||||
isSubmitDisabled() {
|
||||
return this.$v.title.$invalid || this.isCreating;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getTeams();
|
||||
this.getAssignees();
|
||||
this.getLabels();
|
||||
this.getWorkflowStates();
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
onChangeTeam(event) {
|
||||
this.teamId = event.target.value;
|
||||
this.getLabels();
|
||||
this.getWorkflowStates();
|
||||
this.getAssignees();
|
||||
},
|
||||
async getTeams() {
|
||||
const teams = await linearClient.teams();
|
||||
this.teams = teams.nodes;
|
||||
},
|
||||
async getAssignees() {
|
||||
const assignees = await linearClient.users();
|
||||
this.assignees = assignees.nodes;
|
||||
},
|
||||
async getLabels() {
|
||||
const labels = await linearClient.issueLabels({
|
||||
teamId: this.teamId,
|
||||
});
|
||||
this.labels = labels.nodes;
|
||||
},
|
||||
async getWorkflowStates() {
|
||||
const states = await linearClient.workflowStates();
|
||||
this.statuses = states.nodes;
|
||||
},
|
||||
async createIssue() {
|
||||
this.isCreating = true;
|
||||
const response = await linearClient.createIssue({
|
||||
teamId: this.teamId,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
assigneeId: this.assigneeId,
|
||||
priority: this.priority,
|
||||
labelIds: this.labelId ? [this.labelId] : [],
|
||||
});
|
||||
const stringifyResponse = JSON.stringify(response);
|
||||
const {
|
||||
_issue: { id: issueId },
|
||||
} = JSON.parse(stringifyResponse);
|
||||
this.attachmentLinkURL(issueId);
|
||||
this.showAlert('Linear issue created successfully');
|
||||
},
|
||||
async attachmentLinkURL(issueId) {
|
||||
const url =
|
||||
BASE_URL +
|
||||
frontendURL(
|
||||
conversationUrl({
|
||||
accountId: this.accountId,
|
||||
id: this.conversationId,
|
||||
})
|
||||
);
|
||||
await linearClient.attachmentLinkURL(issueId, url);
|
||||
this.isCreating = false;
|
||||
this.onClose();
|
||||
},
|
||||
|
||||
openSearch() {
|
||||
this.showSearchBox = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between group">
|
||||
<!-- <woot-button
|
||||
size="tiny"
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
icon="delete"
|
||||
@click="deleteIssue(issue)"
|
||||
/> -->
|
||||
<a
|
||||
:href="issue.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-block rounded-sm mb-0 break-all py-0.5 text-primary-600"
|
||||
>
|
||||
{{ `${issue.identifier} ${issue.title}` }}
|
||||
</a>
|
||||
<woot-button
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class="!px-2 hover:!bg-transparent hidden dark:hover:!bg-transparent underline group-hover:block"
|
||||
@click="deleteIssue(issue)"
|
||||
>
|
||||
Unlink
|
||||
</woot-button>
|
||||
</div>
|
||||
|
||||
<span class="text-sm font-normal text-slate-900 dark:text-slate-25">
|
||||
{{ issue.description }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between w-full">
|
||||
<span
|
||||
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
Status
|
||||
</span>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<span
|
||||
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||
>
|
||||
Todo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between w-full">
|
||||
<span
|
||||
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
Priority
|
||||
</span>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<span
|
||||
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||
>
|
||||
{{ issue.priorityLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between w-full">
|
||||
<span
|
||||
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
Assignee
|
||||
</span>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<span
|
||||
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||
>
|
||||
Muhsin
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between w-full">
|
||||
<span
|
||||
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
Labels
|
||||
</span>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<span
|
||||
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||
>
|
||||
---
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between w-full">
|
||||
<span
|
||||
class="text-sm sticky top-0 h-fit font-normal tracking-[-0.6%] min-w-[140px] truncate text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
Created At
|
||||
</span>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<span
|
||||
class="text-sm font-normal text-left text-slate-900 dark:text-slate-25 tabular-nums"
|
||||
>
|
||||
{{ formatDate(issue.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { format } from 'date-fns';
|
||||
export default {
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
deleteIssue(issue) {
|
||||
this.$emit('delete', issue);
|
||||
},
|
||||
formatDate(timestamp) {
|
||||
return format(timestamp, 'MMM dd, hh:mm a');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row gap-2">
|
||||
<woot-button
|
||||
v-if="!isLoading && !issues.length"
|
||||
variant="smooth"
|
||||
icon="add"
|
||||
size="tiny"
|
||||
@click="shoeCreateIssuePopup"
|
||||
>
|
||||
Create or Link Issue
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="isLoading" class="flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
|
||||
Loading..
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isLoading && issues.length" class="flex flex-col gap-2">
|
||||
<issue-item
|
||||
v-for="issue in issues"
|
||||
:key="issue.id"
|
||||
:issue="issue"
|
||||
:conversation-id="conversationId"
|
||||
@delete="deleteIssue"
|
||||
/>
|
||||
</div>
|
||||
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
|
||||
<add-issue
|
||||
:conversation-id="conversationId"
|
||||
:account-id="currentAccountId"
|
||||
@close="hideAddPopup"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { LinearClient } from '@linear/sdk';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import IssueItem from './IssueItem.vue';
|
||||
import accountMixin from 'dashboard/mixins/account.js';
|
||||
import AddIssue from './AddIssue.vue';
|
||||
|
||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper.js';
|
||||
|
||||
// const chatwootAPIKey = '';
|
||||
const localAPIKey = '';
|
||||
const linearClient = new LinearClient({
|
||||
apiKey: localAPIKey,
|
||||
});
|
||||
// const BASE_URL = 'https:app.chatwoot.com';
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IssueItem,
|
||||
AddIssue,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
showAddPopup: false,
|
||||
issues: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.loadIssues();
|
||||
},
|
||||
methods: {
|
||||
shoeCreateIssuePopup() {
|
||||
this.showAddPopup = true;
|
||||
},
|
||||
hideAddPopup() {
|
||||
this.showAddPopup = false;
|
||||
this.loadIssues();
|
||||
},
|
||||
async loadIssues() {
|
||||
const url =
|
||||
BASE_URL +
|
||||
frontendURL(
|
||||
conversationUrl({
|
||||
accountId: this.accountId,
|
||||
id: this.conversationId,
|
||||
})
|
||||
);
|
||||
this.isLoading = true;
|
||||
linearClient.attachmentsForURL(url).then(issues => {
|
||||
if (issues.nodes.length) {
|
||||
issues.nodes.map(issue1 => {
|
||||
const linearIssueId = issue1._issue.id;
|
||||
const attachmentId = issue1.id;
|
||||
if (linearIssueId) {
|
||||
linearClient.issue(linearIssueId).then(issue2 => {
|
||||
console.log('Issue:', issue2, attachmentId);
|
||||
this.issues.push({
|
||||
id: issue2.id,
|
||||
title: issue2.title,
|
||||
link: issue2.url,
|
||||
identifier: issue2.identifier,
|
||||
attachmentId,
|
||||
...issue2,
|
||||
});
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
console.log('No issues');
|
||||
}
|
||||
});
|
||||
},
|
||||
async deleteIssue(issue) {
|
||||
const index = this.issues.findIndex(i => i.id === issue.id);
|
||||
if (index > -1) {
|
||||
this.issues.splice(index, 1);
|
||||
}
|
||||
await linearClient.deleteAttachment(issue.attachmentId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.macros_list--empty-state {
|
||||
padding: var(--space-slab);
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.macros_add-button {
|
||||
margin: var(--space-small) auto 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
|
||||
export default {
|
||||
title: {
|
||||
required,
|
||||
},
|
||||
description: {
|
||||
required,
|
||||
},
|
||||
teamId: {},
|
||||
assigneeId: {},
|
||||
};
|
||||
@@ -33,6 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
@@ -49,7 +50,7 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
mixins: [timeMixin, messageFormatterMixin],
|
||||
mixins: [timeMixin, messageFormatterMixin, clickaway],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
|
||||
+2
-1
@@ -34,6 +34,7 @@
|
||||
|
||||
<script>
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
import SearchHeader from './Header.vue';
|
||||
@@ -51,7 +52,7 @@ export default {
|
||||
SearchResults,
|
||||
ArticleView,
|
||||
},
|
||||
mixins: [portalMixin, alertMixin, keyboardEventListenerMixins],
|
||||
mixins: [clickaway, portalMixin, alertMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
selectedPortalSlug: {
|
||||
type: String,
|
||||
|
||||
+3
@@ -138,6 +138,8 @@
|
||||
</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';
|
||||
@@ -150,6 +152,7 @@ export default {
|
||||
WootDropdownMenu,
|
||||
MultiselectDropdownItems,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
|
||||
+2
-1
@@ -109,13 +109,14 @@
|
||||
|
||||
<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],
|
||||
mixins: [alertMixin, clickaway],
|
||||
props: {
|
||||
isSidebarOpen: {
|
||||
type: Boolean,
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
<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,11 +47,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import PortalSwitch from './PortalSwitch.vue';
|
||||
export default {
|
||||
components: {
|
||||
PortalSwitch,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
portals: {
|
||||
type: Array,
|
||||
|
||||
+238
-223
@@ -1,241 +1,256 @@
|
||||
<script setup>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
<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>
|
||||
|
||||
import { defineComponent, reactive, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'dashboard/composables/useI18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { isDomain } from 'shared/helpers/Validators';
|
||||
|
||||
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
|
||||
|
||||
const { t } = useI18n();
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
submitButtonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
slug: '',
|
||||
domain: '',
|
||||
alertMessage: '',
|
||||
|
||||
defineComponent({
|
||||
name: 'PortalSettingsBasicForm',
|
||||
});
|
||||
// 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;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
submitButtonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
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 state = reactive({
|
||||
name: '',
|
||||
slug: '',
|
||||
domain: '',
|
||||
logoUrl: '',
|
||||
avatarBlobId: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
slug: {
|
||||
required,
|
||||
},
|
||||
domain: {
|
||||
isDomain,
|
||||
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 v$ = useVuelidate(rules, state);
|
||||
|
||||
const nameError = computed(() => {
|
||||
if (v$.value.name.$error) {
|
||||
return t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR');
|
||||
}
|
||||
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'));
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
input {
|
||||
@apply mb-1;
|
||||
}
|
||||
.help-text {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+143
-133
@@ -1,140 +1,68 @@
|
||||
<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>
|
||||
<SettingsLayout
|
||||
:title="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.CUSTOMIZATION_PAGE.TITLE')
|
||||
"
|
||||
<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="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 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>
|
||||
</div>
|
||||
<template #footer-right>
|
||||
<div class="flex justify-end">
|
||||
<woot-button
|
||||
:is-loading="isSubmitting"
|
||||
:is-disabled="v$.$invalid"
|
||||
:is-disabled="$v.$invalid"
|
||||
@click="onSubmitClick"
|
||||
>
|
||||
{{
|
||||
@@ -143,12 +71,94 @@ onMounted(() => {
|
||||
)
|
||||
}}
|
||||
</woot-button>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</div>
|
||||
</div>
|
||||
</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;
|
||||
}
|
||||
|
||||
+121
-105
@@ -1,115 +1,15 @@
|
||||
<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 max-w-6xl">
|
||||
<header class="flex items-center justify-between mb-4">
|
||||
<div class="w-full pl-4">
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<label
|
||||
class="mb-0 text-base font-normal text-slate-800 dark:text-slate-100"
|
||||
class="font-normal mb-0 text-base text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TITLE') }}
|
||||
</label>
|
||||
<select
|
||||
:value="currentLocaleCode"
|
||||
class="w-[15%] h-8 mb-0 py-0.5"
|
||||
class="w-[15%] select-locale"
|
||||
@change="changeCurrentCategory"
|
||||
>
|
||||
<option
|
||||
@@ -121,7 +21,7 @@ function changeCurrentCategory(event) {
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="items-center flex-none">
|
||||
<div class="flex-none items-center">
|
||||
<woot-button
|
||||
size="small"
|
||||
variant="smooth"
|
||||
@@ -158,3 +58,119 @@ function changeCurrentCategory(event) {
|
||||
/>
|
||||
</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>
|
||||
|
||||
+3
@@ -36,10 +36,13 @@
|
||||
</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="flex flex-wrap max-w-full px-8 py-4 my-auto">
|
||||
<div class="p-4 max-w-full my-auto flex flex-wrap">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+77
-72
@@ -1,75 +1,3 @@
|
||||
<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"
|
||||
@@ -82,3 +10,80 @@ async function deleteLogo() {
|
||||
@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>
|
||||
|
||||
+48
-45
@@ -1,48 +1,3 @@
|
||||
<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"
|
||||
@@ -54,3 +9,51 @@ async function updatePortalSettings(portalObj) {
|
||||
@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>
|
||||
|
||||
+134
-121
@@ -1,117 +1,6 @@
|
||||
<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="w-full h-full max-w-6xl space-y-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex justify-end">
|
||||
<div class="portal-locales">
|
||||
<div class="button-container">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@@ -122,18 +11,19 @@ function addLocale() {
|
||||
{{ $t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.ADD') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<LocaleItemTable
|
||||
v-if="currentPortal"
|
||||
:locales="locales"
|
||||
:selected-locale-code="currentPortal.meta.default_locale"
|
||||
@change-default-locale="changeDefaultLocale"
|
||||
@delete="deletePortalLocale"
|
||||
/>
|
||||
<div class="locale-container">
|
||||
<locale-item-table
|
||||
:locales="locales"
|
||||
:selected-locale-code="currentPortal.meta.default_locale"
|
||||
@change-default-locale="changeDefaultLocale"
|
||||
@delete="deletePortalLocale"
|
||||
/>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show.sync="isAddLocaleModalOpen"
|
||||
:on-close="closeAddLocaleModal"
|
||||
>
|
||||
<AddLocale
|
||||
<add-locale
|
||||
:show="isAddLocaleModalOpen"
|
||||
:portal="currentPortal"
|
||||
@cancel="closeAddLocaleModal"
|
||||
@@ -141,3 +31,126 @@ function addLocale() {
|
||||
</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>
|
||||
<section class="flex-1">
|
||||
<div class="flex-1">
|
||||
<settings-header
|
||||
button-route="new"
|
||||
:header-title="portalHeaderText"
|
||||
@@ -10,20 +10,16 @@
|
||||
:show-new-button="false"
|
||||
/>
|
||||
<div
|
||||
class="grid grid-cols-[20rem_1fr] w-full h-full overflow-auto rtl:pl-0 rtl:pr-4 bg-slate-50 dark:bg-slate-800 p-5"
|
||||
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"
|
||||
>
|
||||
<woot-wizard
|
||||
class="hidden md:block"
|
||||
class="hidden md:block w-1/4"
|
||||
:global-config="globalConfig"
|
||||
:items="items"
|
||||
/>
|
||||
<div
|
||||
class="w-full p-5 bg-white border border-transparent border-solid rounded-md shadow-sm dark:bg-slate-900 dark:border-transparent"
|
||||
>
|
||||
<router-view />
|
||||
</div>
|
||||
<router-view />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
+72
-60
@@ -1,63 +1,3 @@
|
||||
<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"
|
||||
@@ -69,3 +9,75 @@ async function updatePortalSettings(portalObj) {
|
||||
@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>
|
||||
|
||||
+15
-7
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 w-full h-full max-w-full px-6 pt-3 pb-6 bg-white border border-transparent border-solid dark:bg-slate-900 dark:border-transparent"
|
||||
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"
|
||||
>
|
||||
<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="rounded button success nice"
|
||||
class="button success nice rounded"
|
||||
:to="{
|
||||
name: 'list_all_portals',
|
||||
}"
|
||||
@@ -22,10 +22,18 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script>
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
defineComponent({
|
||||
name: 'PortalSettingsFinish',
|
||||
});
|
||||
export default {
|
||||
components: {
|
||||
EmptyState,
|
||||
},
|
||||
methods: {
|
||||
changeRoute() {
|
||||
this.$router.push({
|
||||
name: 'list_all_portals',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
</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';
|
||||
@@ -67,7 +68,7 @@ export default {
|
||||
InboxOptionMenu,
|
||||
InboxDisplayMenu,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
mixins: [clickaway, alertMixin],
|
||||
props: {
|
||||
isContextMenuOpen: {
|
||||
type: Boolean,
|
||||
|
||||
+2
-1
@@ -115,6 +115,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import NotificationPanelList from './NotificationPanelList.vue';
|
||||
|
||||
@@ -124,7 +125,7 @@ export default {
|
||||
components: {
|
||||
NotificationPanelList,
|
||||
},
|
||||
mixins: [rtlMixin],
|
||||
mixins: [clickaway, rtlMixin],
|
||||
data() {
|
||||
return {
|
||||
pageSize: 15,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</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';
|
||||
@@ -33,7 +34,7 @@ export default {
|
||||
CampaignCard,
|
||||
},
|
||||
|
||||
mixins: [campaignMixin],
|
||||
mixins: [clickaway, campaignMixin],
|
||||
|
||||
props: {
|
||||
campaigns: {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-row justify-between gap-4">
|
||||
<woot-input
|
||||
name="access_token"
|
||||
class="flex-1 focus:[&>input]:!border-slate-200 focus:[&>input]:dark:!border-slate-600 [&>input]:cursor-not-allowed"
|
||||
:styles="{
|
||||
borderRadius: '12px',
|
||||
padding: '6px 12px',
|
||||
fontSize: '14px',
|
||||
marginBottom: '2px',
|
||||
}"
|
||||
type="password"
|
||||
:value="value"
|
||||
readonly
|
||||
/>
|
||||
<form-button
|
||||
type="submit"
|
||||
size="large"
|
||||
icon="text-copy"
|
||||
variant="outline"
|
||||
color-scheme="secondary"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.COPY') }}
|
||||
</form-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import FormButton from 'v3/components/Form/Button.vue';
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['on-copy']);
|
||||
const onClick = () => {
|
||||
emit('on-copy', props.value);
|
||||
};
|
||||
</script>
|
||||
@@ -1,42 +0,0 @@
|
||||
<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>
|
||||
@@ -1,66 +0,0 @@
|
||||
<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>
|
||||
@@ -1,56 +0,0 @@
|
||||
<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>
|
||||
@@ -1,128 +0,0 @@
|
||||
<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>
|
||||
@@ -1,137 +0,0 @@
|
||||
<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>
|
||||
@@ -1,64 +0,0 @@
|
||||
<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,264 +0,0 @@
|
||||
<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 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') }}
|
||||
</h2>
|
||||
<user-profile-picture
|
||||
:src="avatarUrl"
|
||||
:name="name"
|
||||
size="72px"
|
||||
@change="updateProfilePicture"
|
||||
@delete="deleteProfilePicture"
|
||||
/>
|
||||
<user-basic-details
|
||||
:name="name"
|
||||
:display-name="displayName"
|
||||
:email="email"
|
||||
:email-enabled="!globalConfig.disableUserProfileUpdate"
|
||||
@update-user="updateProfile"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form-section
|
||||
:title="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.NOTE')
|
||||
"
|
||||
>
|
||||
<message-signature
|
||||
:message-signature="messageSignature"
|
||||
@update-signature="updateSignature"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section
|
||||
:title="$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
|
||||
:title="$t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE')"
|
||||
>
|
||||
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
|
||||
</form-section>
|
||||
<form-section
|
||||
:title="$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')"
|
||||
:description="
|
||||
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
|
||||
"
|
||||
>
|
||||
<audio-notifications />
|
||||
</form-section>
|
||||
<form-section :title="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
|
||||
<notification-preferences />
|
||||
</form-section>
|
||||
<form-section
|
||||
:title="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE')"
|
||||
:description="
|
||||
useInstallationName(
|
||||
$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
"
|
||||
>
|
||||
<access-token
|
||||
:value="currentUser.access_token"
|
||||
@on-copy="onCopyToken"
|
||||
/>
|
||||
</form-section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import uiSettingsMixin, {
|
||||
isEditorHotKeyEnabled,
|
||||
} from 'dashboard/mixins/uiSettings';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
|
||||
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';
|
||||
import AccessToken from './AccessToken.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MessageSignature,
|
||||
FormSection,
|
||||
UserProfilePicture,
|
||||
UserBasicDetails,
|
||||
HotKeyCard,
|
||||
ChangePassword,
|
||||
NotificationPreferences,
|
||||
AudioNotifications,
|
||||
AccessToken,
|
||||
},
|
||||
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
avatarFile: '',
|
||||
avatarUrl: '',
|
||||
name: '',
|
||||
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: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
if (this.currentUserId) {
|
||||
this.initializeUser();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initializeUser() {
|
||||
this.name = this.currentUser.name;
|
||||
this.email = this.currentUser.email;
|
||||
this.avatarUrl = this.currentUser.avatar_url;
|
||||
this.displayName = this.currentUser.display_name;
|
||||
this.messageSignature = this.currentUser.message_signature;
|
||||
},
|
||||
isEditorHotKeyEnabled,
|
||||
async dispatchUpdate(payload, successMessage, errorMessage) {
|
||||
let alertMessage = '';
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', payload);
|
||||
alertMessage = successMessage;
|
||||
|
||||
return true; // return the value so that the status can be known
|
||||
} catch (error) {
|
||||
alertMessage = error?.response?.data?.error
|
||||
? error.response.data.error
|
||||
: errorMessage;
|
||||
|
||||
return false; // return the value so that the status can be known
|
||||
} finally {
|
||||
this.showAlert(alertMessage);
|
||||
}
|
||||
},
|
||||
async updateProfile(userAttributes) {
|
||||
const { name, email, displayName } = userAttributes;
|
||||
const hasEmailChanged = this.currentUser.email !== email;
|
||||
this.name = name || this.name;
|
||||
this.email = email || this.email;
|
||||
this.displayName = displayName || this.displayName;
|
||||
|
||||
const updatePayload = {
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
displayName: this.displayName,
|
||||
avatar: this.avatarFile,
|
||||
};
|
||||
|
||||
const success = await this.dispatchUpdate(
|
||||
updatePayload,
|
||||
hasEmailChanged
|
||||
? this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
|
||||
: this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS'),
|
||||
this.$t('RESET_PASSWORD.API.ERROR_MESSAGE')
|
||||
);
|
||||
|
||||
if (hasEmailChanged && success) clearCookiesOnLogout();
|
||||
},
|
||||
async updateSignature(signature) {
|
||||
const payload = { message_signature: signature };
|
||||
let successMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_SUCCESS'
|
||||
);
|
||||
let errorMessage = this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.API_ERROR'
|
||||
);
|
||||
|
||||
await this.dispatchUpdate(payload, successMessage, errorMessage);
|
||||
},
|
||||
updateProfilePicture({ file, url }) {
|
||||
this.avatarFile = file;
|
||||
this.avatarUrl = url;
|
||||
},
|
||||
async deleteProfilePicture() {
|
||||
try {
|
||||
await this.$store.dispatch('deleteAvatar');
|
||||
this.avatarUrl = '';
|
||||
this.avatarFile = '';
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.AVATAR_DELETE_SUCCESS'));
|
||||
} catch (error) {
|
||||
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')
|
||||
);
|
||||
},
|
||||
async onCopyToken(value) {
|
||||
await copyTextToClipboard(value);
|
||||
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<form class="flex flex-col gap-6" @submit.prevent="updateSignature()">
|
||||
<woot-message-editor
|
||||
id="message-signature-input"
|
||||
v-model="signature"
|
||||
class="message-editor h-[10rem] !px-3"
|
||||
:is-format-mode="true"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE.PLACEHOLDER')"
|
||||
:enabled-menu-options="customEditorMenuList"
|
||||
:enable-suggestions="false"
|
||||
:show-image-resize-toolbar="true"
|
||||
/>
|
||||
<form-button
|
||||
type="submit"
|
||||
color-scheme="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.BTN_TEXT') }}
|
||||
</form-button>
|
||||
</form>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import { MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
import FormButton from 'v3/components/Form/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
messageSignature: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const customEditorMenuList = MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS;
|
||||
const signature = ref(props.messageSignature);
|
||||
const emit = defineEmits(['update-signature']);
|
||||
|
||||
watch(
|
||||
() => props.messageSignature,
|
||||
newValue => {
|
||||
signature.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const updateSignature = () => {
|
||||
emit('update-signature', signature.value);
|
||||
};
|
||||
</script>
|
||||
@@ -1,53 +0,0 @@
|
||||
<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>
|
||||
-284
@@ -1,284 +0,0 @@
|
||||
<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>
|
||||
@@ -1,135 +0,0 @@
|
||||
<template>
|
||||
<form class="flex flex-col gap-4" @submit.prevent="updateUser('profile')">
|
||||
<woot-input
|
||||
v-model="userName"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.userName.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
:error="`${
|
||||
$v.userName.$error ? $t('PROFILE_SETTINGS.FORM.NAME.ERROR') : ''
|
||||
}`"
|
||||
@input="$v.userName.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="userDisplayName"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.userDisplayName.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.PLACEHOLDER')"
|
||||
:error="`${
|
||||
$v.userDisplayName.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.ERROR')
|
||||
: ''
|
||||
}`"
|
||||
@input="$v.userDisplayName.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="emailEnabled"
|
||||
v-model="userEmail"
|
||||
:styles="inputStyles"
|
||||
:class="{ error: $v.userEmail.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.EMAIL.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
|
||||
:error="`${
|
||||
$v.userEmail.$error ? $t('PROFILE_SETTINGS.FORM.EMAIL.ERROR') : ''
|
||||
}`"
|
||||
@input="$v.userEmail.$touch"
|
||||
/>
|
||||
<form-button
|
||||
type="submit"
|
||||
color-scheme="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.BTN_TEXT') }}
|
||||
</form-button>
|
||||
</form>
|
||||
</template>
|
||||
<script>
|
||||
import FormButton from 'v3/components/Form/Button.vue';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
export default {
|
||||
components: {
|
||||
FormButton,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
emailEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userName: this.name,
|
||||
userDisplayName: this.displayName,
|
||||
userEmail: this.email,
|
||||
inputStyles: {
|
||||
borderRadius: '12px',
|
||||
padding: '6px 12px',
|
||||
fontSize: '14px',
|
||||
marginBottom: '2px',
|
||||
},
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
userName: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
},
|
||||
userDisplayName: {},
|
||||
userEmail: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
name: {
|
||||
handler(value) {
|
||||
this.userName = value;
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
displayName: {
|
||||
handler(value) {
|
||||
this.userDisplayName = value;
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
email: {
|
||||
handler(value) {
|
||||
this.userEmail = value;
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateUser() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||
return;
|
||||
}
|
||||
this.$emit('update-user', {
|
||||
name: this.userName,
|
||||
displayName: this.userDisplayName,
|
||||
email: this.userEmail,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm font-medium text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PICTURE') }}
|
||||
</span>
|
||||
<profile-avatar
|
||||
:src="src"
|
||||
:name="userNameWithoutEmoji"
|
||||
@change="updateProfilePicture"
|
||||
@delete="deleteProfilePicture"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import ProfileAvatar from 'v3/components/Form/ProfileAvatar.vue';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['change', 'delete']);
|
||||
|
||||
const userNameWithoutEmoji = computed(() => removeEmoji(props.name));
|
||||
|
||||
const updateProfilePicture = e => {
|
||||
emits('change', e);
|
||||
};
|
||||
|
||||
const deleteProfilePicture = () => {
|
||||
emits('delete');
|
||||
};
|
||||
</script>
|
||||
@@ -1,52 +0,0 @@
|
||||
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',
|
||||
},
|
||||
];
|
||||
@@ -1,20 +0,0 @@
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
const Index = () => import('./Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/personal'),
|
||||
name: 'personal_settings',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: Index,
|
||||
props: {
|
||||
headerTitle: 'PROFILE_SETTINGS.TITLE',
|
||||
icon: 'edit',
|
||||
showNewButton: false,
|
||||
showSidemenuIcon: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
+39
-16
@@ -1,10 +1,24 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-wrap w-full gap-3 md:flex-row">
|
||||
<woot-date-picker @dateRangeChanged="onDateRangeChange" />
|
||||
<reports-filters-date-range
|
||||
class="sm:min-w-[200px] tiny h-8"
|
||||
@on-range-change="onDateRangeChange"
|
||||
/>
|
||||
<woot-date-range-picker
|
||||
v-if="isDateRangeSelected"
|
||||
show-range
|
||||
class="no-margin auto-width sm:min-w-[240px] small h-8"
|
||||
:value="customDateRange"
|
||||
:confirm-text="$t('REPORT.CUSTOM_DATE_RANGE.CONFIRM')"
|
||||
:placeholder="$t('REPORT.CUSTOM_DATE_RANGE.PLACEHOLDER')"
|
||||
@change="onCustomDateRangeChange"
|
||||
/>
|
||||
<SLA-filter @filter-change="emitFilterChange" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
import ReportsFiltersDateRange from '../Filters/DateRange.vue';
|
||||
import SLAFilter from '../SLA/SLAFilter.vue';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import { DATE_RANGE_OPTIONS } from '../../constants';
|
||||
@@ -12,6 +26,8 @@ import { getUnixStartOfDay, getUnixEndOfDay } from 'helpers/DateHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDateRangePicker,
|
||||
ReportsFiltersDateRange,
|
||||
SLAFilter,
|
||||
},
|
||||
|
||||
@@ -23,11 +39,25 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDateRangeSelected() {
|
||||
return (
|
||||
this.selectedDateRange.id === DATE_RANGE_OPTIONS.CUSTOM_DATE_RANGE.id
|
||||
);
|
||||
},
|
||||
to() {
|
||||
return getUnixEndOfDay(this.customDateRange[1]);
|
||||
if (this.isDateRangeSelected) {
|
||||
return getUnixEndOfDay(this.customDateRange[1]);
|
||||
}
|
||||
return getUnixEndOfDay(new Date());
|
||||
},
|
||||
from() {
|
||||
return getUnixStartOfDay(this.customDateRange[0]);
|
||||
if (this.isDateRangeSelected) {
|
||||
return getUnixStartOfDay(this.customDateRange[0]);
|
||||
}
|
||||
|
||||
const { offset } = this.selectedDateRange;
|
||||
const fromDate = subDays(new Date(), offset);
|
||||
return getUnixStartOfDay(fromDate);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -36,20 +66,9 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setInitialRange();
|
||||
this.emitChange();
|
||||
},
|
||||
methods: {
|
||||
setInitialRange() {
|
||||
const { offset } = this.selectedDateRange;
|
||||
const fromDate = subDays(new Date(), offset);
|
||||
const from = getUnixStartOfDay(fromDate);
|
||||
const to = getUnixEndOfDay(new Date());
|
||||
this.$emit('filter-change', {
|
||||
from,
|
||||
to,
|
||||
...this.selectedGroupByFilter,
|
||||
});
|
||||
},
|
||||
emitChange() {
|
||||
const { from, to } = this;
|
||||
this.$emit('filter-change', {
|
||||
@@ -62,7 +81,11 @@ export default {
|
||||
this.selectedGroupByFilter = params;
|
||||
this.emitChange();
|
||||
},
|
||||
onDateRangeChange(value) {
|
||||
onDateRangeChange(selectedRange) {
|
||||
this.selectedDateRange = selectedRange;
|
||||
this.emitChange();
|
||||
},
|
||||
onCustomDateRangeChange(value) {
|
||||
this.customDateRange = value;
|
||||
this.emitChange();
|
||||
},
|
||||
|
||||
+3
@@ -22,11 +22,14 @@
|
||||
</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,
|
||||
|
||||
@@ -18,7 +18,6 @@ import reports from './reports/reports.routes';
|
||||
import store from '../../../store';
|
||||
import sla from './sla/sla.routes';
|
||||
import teams from './teams/teams.routes';
|
||||
import personal from './personal/personal.routes';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -51,6 +50,5 @@ export default {
|
||||
...reports.routes,
|
||||
...sla.routes,
|
||||
...teams.routes,
|
||||
...personal.routes,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import SearchSuggestions from './SearchSuggestions.vue';
|
||||
import PublicSearchInput from './PublicSearchInput.vue';
|
||||
|
||||
@@ -33,6 +35,7 @@ export default {
|
||||
PublicSearchInput,
|
||||
SearchSuggestions,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z",
|
||||
"M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z"
|
||||
],
|
||||
"calendar-outline": "M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h11.5Zm1.75 5.5h-15v9.25c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75V8.5Zm-11.75 6a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm4.25 0a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm-4.25-4a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm4.25 0a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm4.25 0a1.25 1.25 0 1 1 0 2.5a1.25 1.25 0 0 1 0-2.5Zm1.5-6H6.25A1.75 1.75 0 0 0 4.5 6.25V7h15v-.75a1.75 1.75 0 0 0-1.75-1.75Z",
|
||||
"call-outline": "m7.056 2.418 1.167-.351a2.75 2.75 0 0 1 3.302 1.505l.902 2.006a2.75 2.75 0 0 1-.633 3.139L10.3 10.11a.25.25 0 0 0-.078.155c-.044.397.225 1.17.845 2.245.451.781.86 1.33 1.207 1.637.242.215.375.261.432.245l2.01-.615a2.75 2.75 0 0 1 3.034 1.02l1.281 1.776a2.75 2.75 0 0 1-.339 3.605l-.886.84a3.75 3.75 0 0 1-3.587.889c-2.754-.769-5.223-3.093-7.435-6.924-2.215-3.836-2.992-7.14-2.276-9.913a3.75 3.75 0 0 1 2.548-2.652Zm.433 1.437a2.25 2.25 0 0 0-1.529 1.59c-.602 2.332.087 5.261 2.123 8.788 2.033 3.522 4.222 5.582 6.54 6.23a2.25 2.25 0 0 0 2.151-.534l.887-.84a1.25 1.25 0 0 0 .154-1.639l-1.28-1.775a1.25 1.25 0 0 0-1.38-.464l-2.015.617c-1.17.348-2.232-.593-3.372-2.568C9 11.93 8.642 10.9 8.731 10.099c.047-.416.24-.8.546-1.086l1.494-1.393a1.25 1.25 0 0 0 .288-1.427l-.902-2.006a1.25 1.25 0 0 0-1.5-.684l-1.168.352Z",
|
||||
"chat-help-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5Zm0 12a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm0-8.75a2.75 2.75 0 0 1 2.75 2.75c0 1.01-.297 1.574-1.051 2.359l-.169.171c-.622.622-.78.886-.78 1.47a.75.75 0 0 1-1.5 0c0-1.01.297-1.574 1.051-2.359l.169-.171c.622-.622.78-.886.78-1.47a1.25 1.25 0 0 0-2.493-.128l-.007.128a.75.75 0 0 1-1.5 0A2.75 2.75 0 0 1 12 6.75Z",
|
||||
"chat-multiple-outline": "M9.562 3a7.5 7.5 0 0 0-6.798 10.673l-.724 2.842a1.25 1.25 0 0 0 1.504 1.524c.75-.18 1.903-.457 2.93-.702A7.5 7.5 0 1 0 9.561 3Zm-6 7.5a6 6 0 1 1 3.33 5.375l-.244-.121-.264.063c-.923.22-1.99.475-2.788.667l.69-2.708.07-.276-.13-.253a5.971 5.971 0 0 1-.664-2.747Zm11 10.5c-1.97 0-3.762-.759-5.1-2h.1c.718 0 1.415-.089 2.08-.257.865.482 1.86.757 2.92.757.96 0 1.866-.225 2.67-.625l.243-.121.264.063c.922.22 1.966.445 2.74.61-.175-.751-.414-1.756-.642-2.651l-.07-.276.13-.253a5.971 5.971 0 0 0 .665-2.747 5.995 5.995 0 0 0-2.747-5.042 8.44 8.44 0 0 0-.8-2.047 7.503 7.503 0 0 1 4.344 10.263c.253 1.008.509 2.1.671 2.803a1.244 1.244 0 0 1-1.467 1.5 132.62 132.62 0 0 1-2.913-.64 7.476 7.476 0 0 1-3.088.663Z",
|
||||
@@ -273,7 +272,5 @@
|
||||
"chevrons-left-outline": ["m11 17-5-5 5-5", "m18 17-5-5 5-5"],
|
||||
"chevron-left-single-outline": "m15 18-6-6 6-6",
|
||||
"chevrons-right-outline": ["m6 17 5-5-5-5", "m13 17 5-5-5-5"],
|
||||
"chevron-right-single-outline": "m9 18 6-6-6-6",
|
||||
"avatar-upload-outline": "M19.754 11a.75.75 0 0 1 .743.648l.007.102v7a3.25 3.25 0 0 1-3.065 3.246l-.185.005h-11a3.25 3.25 0 0 1-3.244-3.066l-.006-.184V11.75a.75.75 0 0 1 1.494-.102l.006.102v7a1.75 1.75 0 0 0 1.607 1.745l.143.006h11A1.75 1.75 0 0 0 19 18.894l.005-.143V11.75a.75.75 0 0 1 .75-.75ZM6.22 7.216l4.996-4.996a.75.75 0 0 1 .976-.073l.084.072l5.005 4.997a.75.75 0 0 1-.976 1.134l-.084-.073l-3.723-3.716l.001 11.694a.75.75 0 0 1-.648.743l-.102.007a.75.75 0 0 1-.743-.648L11 16.255V4.558L7.28 8.277a.75.75 0 0 1-.976.073l-.084-.073a.75.75 0 0 1-.073-.977l.073-.084l4.996-4.996L6.22 7.216Z",
|
||||
"text-copy-outline": "M5.503 4.627L5.5 6.75v10.504a3.25 3.25 0 0 0 3.25 3.25h8.616a2.25 2.25 0 0 1-2.122 1.5H8.75A4.75 4.75 0 0 1 4 17.254V6.75c0-.98.627-1.815 1.503-2.123M17.75 2A2.25 2.25 0 0 1 20 4.25v13a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-13A2.25 2.25 0 0 1 8.75 2zm0 1.5h-9a.75.75 0 0 0-.75.75v13c0 .414.336.75.75.75h9a.75.75 0 0 0 .75-.75v-13a.75.75 0 0 0-.75-.75"
|
||||
"chevron-right-single-outline": "m9 18 6-6-6-6"
|
||||
}
|
||||
|
||||
@@ -74,11 +74,13 @@
|
||||
<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,
|
||||
|
||||
@@ -39,14 +39,12 @@ export default {
|
||||
ArrowUp: {
|
||||
action: e => {
|
||||
e.preventDefault();
|
||||
console.log('ArrowUp in Dropdown Menu');
|
||||
this.focusPreviousButton(menuButtons);
|
||||
},
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
ArrowDown: {
|
||||
action: e => {
|
||||
console.log('ArrowDown in Dropdown Menu');
|
||||
e.preventDefault();
|
||||
this.focusNextButton(menuButtons);
|
||||
},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
import { isActiveElementTypeable, isEscape } from '../helpers/KeyboardHelpers';
|
||||
|
||||
import { createKeybindingsHandler } from 'tinykeys';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
// this is a store that stores the handler globally, and only gets reset on reload
|
||||
const taggedHandlers = [];
|
||||
@@ -13,26 +11,22 @@ export default {
|
||||
if (events) {
|
||||
const wrappedEvents = this.wrapEventsInKeybindingsHandler(events);
|
||||
const keydownHandler = createKeybindingsHandler(wrappedEvents);
|
||||
this.addEventHandler(keydownHandler);
|
||||
this.appendToHandler(keydownHandler);
|
||||
document.addEventListener('keydown', 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: {
|
||||
addEventHandler(keydownHandler) {
|
||||
appendToHandler(keydownHandler) {
|
||||
const indexToAppend = taggedHandlers.push(keydownHandler) - 1;
|
||||
const root = this.$el;
|
||||
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;
|
||||
}
|
||||
root.dataset.keydownHandlerIndex = indexToAppend;
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return null;
|
||||
@@ -47,31 +41,22 @@ export default {
|
||||
},
|
||||
keydownWrapper(handler) {
|
||||
return e => {
|
||||
const isFunction = typeof handler === 'function';
|
||||
const actionToPerform = isFunction ? handler : handler.action;
|
||||
const allowOnFocusedInput = isFunction
|
||||
? false
|
||||
: handler.allowOnFocusedInput;
|
||||
const actionToPerform =
|
||||
typeof handler === 'function' ? handler : handler.action;
|
||||
const allowOnFocusedInput =
|
||||
typeof handler === 'function' ? false : handler.allowOnFocusedInput;
|
||||
|
||||
try {
|
||||
const isTypeable = isActiveElementTypeable(e);
|
||||
const isTypeable = isActiveElementTypeable(e);
|
||||
|
||||
if (isTypeable) {
|
||||
if (isEscape(e)) e.target.blur();
|
||||
if (!allowOnFocusedInput) return;
|
||||
if (isTypeable) {
|
||||
if (isEscape(e)) {
|
||||
e.target.blur();
|
||||
}
|
||||
|
||||
actionToPerform(e);
|
||||
} catch (err) {
|
||||
// ignore errors
|
||||
Sentry.captureException(err, {
|
||||
context: {
|
||||
component: this.$options?.name,
|
||||
isFunction: isFunction,
|
||||
allowOnFocusedInput: allowOnFocusedInput,
|
||||
},
|
||||
});
|
||||
if (!allowOnFocusedInput) return;
|
||||
}
|
||||
|
||||
actionToPerform(e);
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
class="inline-flex items-center gap-1 text-sm font-medium reset-base rounded-xl w-fit"
|
||||
:class="buttonClasses"
|
||||
v-bind="$attrs"
|
||||
@click="onClick"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="icon && !trailingIcon"
|
||||
@@ -57,10 +56,9 @@ const props = defineProps({
|
||||
const attrs = useAttrs();
|
||||
|
||||
const baseClasses = {
|
||||
outline: 'outline outline-1 -outline-offset-1',
|
||||
ghost: 'hover:text-600 active:text-600 focus:outline focus:outline-offset-1',
|
||||
solid:
|
||||
'hover:bg-700 active:bg-700 focus:outline focus:outline-offset-1 focus:outline-2',
|
||||
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',
|
||||
};
|
||||
|
||||
const colorClass = computed(() => {
|
||||
@@ -70,19 +68,19 @@ const colorClass = computed(() => {
|
||||
|
||||
const styleMap = {
|
||||
primary: {
|
||||
outline: `${baseClasses.outline} outline-primary-400 hover:text-primary-600 active:text-primary-600`,
|
||||
ghost: `${baseClasses.ghost} focus:outline-primary-400`,
|
||||
solid: `${baseClasses.solid} bg-primary-600 text-white focus:outline-primary-400`,
|
||||
outline: `${baseClasses.outline} outline-primary-400 hover:text-primary-600 active:text-primary-600 focus:ring-primary-400`,
|
||||
ghost: `${baseClasses.ghost} focus:ring-primary-400`,
|
||||
solid: `${baseClasses.solid} bg-primary-600 text-white focus:ring-primary-400`,
|
||||
},
|
||||
secondary: {
|
||||
outline: `${baseClasses.outline} outline-ash-400 hover:text-ash-900 active:text-ash-900 `,
|
||||
ghost: `${baseClasses.ghost} focus:outline-ash-400`,
|
||||
solid: `${baseClasses.solid} bg-ash-100 text-ash-900 focus:outline-ash-400`,
|
||||
outline: `${baseClasses.outline} outline-ash-400 hover:text-ash-600 active:text-ash-600 focus:ring-ash-400`,
|
||||
ghost: `${baseClasses.ghost} focus:ring-ash-400`,
|
||||
solid: `${baseClasses.solid} bg-ash-100 text-ash-900 focus:ring-ash-400`,
|
||||
},
|
||||
danger: {
|
||||
outline: `${baseClasses.outline} outline-ruby-400 hover:text-ruby-600 active:text-ruby-600`,
|
||||
ghost: `${baseClasses.ghost} focus:outline-ruby-400`,
|
||||
solid: `${baseClasses.solid} bg-ruby-600 text-white focus:outline-ruby-400`,
|
||||
outline: `${baseClasses.outline} outline-ruby-400 hover:text-ruby-600 active:text-ruby-600 focus:ring-ruby-400`,
|
||||
ghost: `${baseClasses.ghost} focus:ring-ruby-400`,
|
||||
solid: `${baseClasses.solid} bg-ruby-600 text-white focus:ring-ruby-400`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -98,13 +96,4 @@ const sizeClass = computed(() => {
|
||||
});
|
||||
|
||||
const buttonClasses = computed(() => [colorClass.value, sizeClass.value]);
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const onClick = () => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
emit('click');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user