Merge branch 'develop' into deploy-apr-15
This commit is contained in:
@@ -8,8 +8,8 @@
|
||||
>
|
||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
<template v-if="currentAccountId">
|
||||
<pending-email-verification-banner />
|
||||
<payment-pending-banner />
|
||||
<pending-email-verification-banner v-if="hideOnOnboardingView" />
|
||||
<payment-pending-banner v-if="hideOnOnboardingView" />
|
||||
<upgrade-banner />
|
||||
</template>
|
||||
<transition name="fade" mode="out-in">
|
||||
@@ -38,6 +38,7 @@ import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import { setColorTheme } from './helper/themeHelper';
|
||||
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
@@ -79,6 +80,9 @@ export default {
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
return accounts.length > 0;
|
||||
},
|
||||
hideOnOnboardingView() {
|
||||
return !isOnOnboardingView(this.$route);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class Agents extends ApiClient {
|
||||
constructor() {
|
||||
super('agents', { accountScoped: true });
|
||||
}
|
||||
|
||||
bulkInvite({ emails }) {
|
||||
return axios.post(`${this.url}/bulk_create`, {
|
||||
emails,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Agents();
|
||||
|
||||
@@ -77,8 +77,8 @@ class ContactAPI extends ApiClient {
|
||||
return axios.delete(`${this.url}/${contactId}/avatar`);
|
||||
}
|
||||
|
||||
exportContacts() {
|
||||
return axios.get(`${this.url}/export`);
|
||||
exportContacts(queryPayload) {
|
||||
return axios.post(`${this.url}/export`, queryPayload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,13 @@ class NotificationsAPI extends ApiClient {
|
||||
}
|
||||
|
||||
get({ page, status, type, sortOrder }) {
|
||||
const includesFilter = [status, type].filter(value => !!value);
|
||||
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
page,
|
||||
status,
|
||||
type,
|
||||
sort_order: sortOrder,
|
||||
includes: includesFilter,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,6 +84,24 @@ class ReportsAPI extends ApiClient {
|
||||
params: { since, until, business_hours: businessHours },
|
||||
});
|
||||
}
|
||||
|
||||
getBotMetrics({ from, to } = {}) {
|
||||
return axios.get(`${this.url}/bot_metrics`, {
|
||||
params: { since: from, until: to },
|
||||
});
|
||||
}
|
||||
|
||||
getBotSummary({ from, to, groupBy, businessHours } = {}) {
|
||||
return axios.get(`${this.url}/bot_summary`, {
|
||||
params: {
|
||||
since: from,
|
||||
until: to,
|
||||
type: 'account',
|
||||
group_by: groupBy,
|
||||
business_hours: businessHours,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportsAPI();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class SlaAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('sla_policies', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default new SlaAPI();
|
||||
@@ -0,0 +1,78 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class SLAReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('applied_slas', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({
|
||||
from,
|
||||
to,
|
||||
assigned_agent_id,
|
||||
inbox_id,
|
||||
team_id,
|
||||
sla_policy_id,
|
||||
label_list,
|
||||
page,
|
||||
} = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
since: from,
|
||||
until: to,
|
||||
assigned_agent_id,
|
||||
inbox_id,
|
||||
team_id,
|
||||
sla_policy_id,
|
||||
label_list,
|
||||
page,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
download({
|
||||
from,
|
||||
to,
|
||||
assigned_agent_id,
|
||||
inbox_id,
|
||||
team_id,
|
||||
sla_policy_id,
|
||||
label_list,
|
||||
} = {}) {
|
||||
return axios.get(`${this.url}/download`, {
|
||||
params: {
|
||||
since: from,
|
||||
until: to,
|
||||
assigned_agent_id,
|
||||
inbox_id,
|
||||
team_id,
|
||||
label_list,
|
||||
sla_policy_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics({
|
||||
from,
|
||||
to,
|
||||
assigned_agent_id,
|
||||
inbox_id,
|
||||
team_id,
|
||||
label_list,
|
||||
sla_policy_id,
|
||||
} = {}) {
|
||||
return axios.get(`${this.url}/metrics`, {
|
||||
params: {
|
||||
since: from,
|
||||
until: to,
|
||||
assigned_agent_id,
|
||||
inbox_id,
|
||||
label_list,
|
||||
team_id,
|
||||
sla_policy_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new SLAReportsAPI();
|
||||
@@ -10,4 +10,29 @@ describe('#AgentAPI', () => {
|
||||
expect(agents).toHaveProperty('update');
|
||||
expect(agents).toHaveProperty('delete');
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#bulkInvite', () => {
|
||||
agents.bulkInvite({ emails: ['hello@hi.com'] });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/agents/bulk_create',
|
||||
{
|
||||
emails: ['hello@hi.com'],
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,20 +27,36 @@ describe('#NotificationAPI', () => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
status: 'read',
|
||||
type: 'Conversation',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
describe('#get', () => {
|
||||
it('generates the API call if both params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
status: 'read',
|
||||
type: 'Conversation',
|
||||
sort_order: 'desc',
|
||||
},
|
||||
status: 'snoozed',
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['snoozed', 'read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('generates the API call if one of the params are available', () => {
|
||||
notificationsAPI.get({
|
||||
page: 1,
|
||||
type: 'read',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', {
|
||||
params: {
|
||||
page: 1,
|
||||
sort_order: 'desc',
|
||||
includes: ['read'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -111,6 +111,40 @@ describe('#Reports API', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('#getBotMetrics', () => {
|
||||
reportsAPI.getBotMetrics({ from: 1621103400, to: 1621621800 });
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/bot_metrics',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getBotSummary', () => {
|
||||
reportsAPI.getBotSummary({
|
||||
from: 1621103400,
|
||||
to: 1621621800,
|
||||
groupBy: 'date',
|
||||
businessHours: true,
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/bot_summary',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
group_by: 'date',
|
||||
business_hours: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getConversationMetric', () => {
|
||||
reportsAPI.getConversationMetric('account');
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import SLAReportsAPI from '../slaReports';
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
describe('#SLAReports API', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(SLAReportsAPI).toBeInstanceOf(ApiClient);
|
||||
expect(SLAReportsAPI.apiVersion).toBe('/api/v1');
|
||||
expect(SLAReportsAPI).toHaveProperty('get');
|
||||
expect(SLAReportsAPI).toHaveProperty('getMetrics');
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('#get', () => {
|
||||
SLAReportsAPI.get({
|
||||
page: 1,
|
||||
from: 1622485800,
|
||||
to: 1623695400,
|
||||
assigned_agent_id: 1,
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
sla_policy_id: 1,
|
||||
label_list: ['label1'],
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/applied_slas', {
|
||||
params: {
|
||||
page: 1,
|
||||
since: 1622485800,
|
||||
until: 1623695400,
|
||||
assigned_agent_id: 1,
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
sla_policy_id: 1,
|
||||
label_list: ['label1'],
|
||||
},
|
||||
});
|
||||
});
|
||||
it('#getMetrics', () => {
|
||||
SLAReportsAPI.getMetrics({
|
||||
from: 1622485800,
|
||||
to: 1623695400,
|
||||
assigned_agent_id: 1,
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
sla_policy_id: 1,
|
||||
label_list: ['label1'],
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/applied_slas/metrics',
|
||||
{
|
||||
params: {
|
||||
since: 1622485800,
|
||||
until: 1623695400,
|
||||
assigned_agent_id: 1,
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
sla_policy_id: 1,
|
||||
label_list: ['label1'],
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
it('#download', () => {
|
||||
SLAReportsAPI.download({
|
||||
from: 1622485800,
|
||||
to: 1623695400,
|
||||
assigned_agent_id: 1,
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
sla_policy_id: 1,
|
||||
label_list: ['label1'],
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/applied_slas/download',
|
||||
{
|
||||
params: {
|
||||
since: 1622485800,
|
||||
until: 1623695400,
|
||||
assigned_agent_id: 1,
|
||||
inbox_id: 1,
|
||||
team_id: 1,
|
||||
sla_policy_id: 1,
|
||||
label_list: ['label1'],
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,13 @@
|
||||
@import '~vue2-datepicker/scss/index';
|
||||
|
||||
.date-picker {
|
||||
// To be removed one SLA reports date picker is created
|
||||
&.small {
|
||||
.mx-input {
|
||||
@apply h-8 text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-margin {
|
||||
.mx-input {
|
||||
@apply mb-0;
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
.button {
|
||||
font-family: $body-font-family;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
&.round {
|
||||
border-radius: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: var(--space-small);
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
code {
|
||||
border: 0;
|
||||
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
|
||||
'"Liberation Mono"', '"Courier New"', 'monospace';
|
||||
font-size: $font-size-mini;
|
||||
|
||||
&.hljs {
|
||||
background: $color-background;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: $space-two;
|
||||
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
.text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// remove when grid gutters are fixed
|
||||
.columns.with-right-space {
|
||||
padding-right: var(--space-normal);
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
|
||||
.padding-right-small {
|
||||
padding-right: var(--space-one);
|
||||
}
|
||||
@@ -1,623 +0,0 @@
|
||||
// Foundation for Sites Settings
|
||||
// -----------------------------
|
||||
//
|
||||
// Table of Contents:
|
||||
//
|
||||
// 1. Global
|
||||
// 2. Breakpoints
|
||||
// 3. The Grid
|
||||
// 4. Base Typography
|
||||
// 5. Typography Helpers
|
||||
// 6. Abide
|
||||
// 7. Accordion
|
||||
// 8. Accordion Menu
|
||||
// 9. Badge
|
||||
// 10. Breadcrumbs
|
||||
// 11. Button
|
||||
// 12. Button Group
|
||||
// 13. Callout
|
||||
// 14. Card
|
||||
// 15. Close Button
|
||||
// 16. Drilldown
|
||||
// 17. Dropdown
|
||||
// 18. Dropdown Menu
|
||||
// 19. Forms
|
||||
// 20. Label
|
||||
// 21. Media Object
|
||||
// 22. Menu
|
||||
// 23. Meter
|
||||
// 24. Off-canvas
|
||||
// 25. Orbit
|
||||
// 26. Pagination
|
||||
// 27. Progress Bar
|
||||
// 28. Responsive Embed
|
||||
// 29. Reveal
|
||||
// 30. Slider
|
||||
// 31. Switch
|
||||
// 32. Table
|
||||
// 33. Tabs
|
||||
// 34. Thumbnail
|
||||
// 35. Title Bar
|
||||
// 36. Tooltip
|
||||
// 37. Top Bar
|
||||
|
||||
@import '~foundation-sites/scss/util/util';
|
||||
// 1. Global
|
||||
// ---------
|
||||
|
||||
// Disable contrast warnings in Foundation.
|
||||
$contrast-warnings: false;
|
||||
|
||||
$global-font-size: 16px;
|
||||
$global-width: 100%;
|
||||
$global-lineheight: 1.5;
|
||||
$foundation-palette: (primary: $color-woot,
|
||||
secondary: #5d7592,
|
||||
success: #44ce4b,
|
||||
warning: #ffc532,
|
||||
alert: #ff382d);
|
||||
$light-gray: #c0ccda;
|
||||
$medium-gray: #8492a6;
|
||||
$dark-gray: $color-gray;
|
||||
$black: #000;
|
||||
$white: #fff;
|
||||
$body-background: $white;
|
||||
$body-font-color: $color-body;
|
||||
$body-font-family: 'PlusJakarta',
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
"Helvetica Neue",
|
||||
Tahoma,
|
||||
Arial,
|
||||
sans-serif;
|
||||
$body-antialiased: true;
|
||||
$global-margin: $space-small;
|
||||
$global-padding: $space-small;
|
||||
$global-weight-normal: normal;
|
||||
$global-weight-bold: bold;
|
||||
$global-radius: 0;
|
||||
$global-text-direction: ltr;
|
||||
$global-flexbox: false;
|
||||
$print-transparent-backgrounds: true;
|
||||
|
||||
@include add-foundation-colors;
|
||||
|
||||
// 2. Breakpoints
|
||||
// --------------
|
||||
|
||||
$breakpoints: (small: 0,
|
||||
medium: 640px,
|
||||
large: 1024px,
|
||||
xlarge: 1200px,
|
||||
xxlarge: 1400px,
|
||||
xxxlarge: 1600px,
|
||||
);
|
||||
$print-breakpoint: large;
|
||||
$breakpoint-classes: (small medium large);
|
||||
|
||||
// 3. The Grid
|
||||
// -----------
|
||||
|
||||
$grid-row-width: $global-width;
|
||||
$grid-column-count: 12;
|
||||
$grid-column-gutter: (small: $zero,
|
||||
medium: $zero);
|
||||
$grid-column-align-edge: true;
|
||||
$block-grid-max: 8;
|
||||
|
||||
// 4. Base Typography
|
||||
// ------------------
|
||||
|
||||
$header-font-family: $body-font-family;
|
||||
$header-font-weight: $font-weight-medium;
|
||||
$header-font-style: normal;
|
||||
$font-family-monospace: $body-font-family;
|
||||
$header-color: $color-heading;
|
||||
$header-lineheight: 1.4;
|
||||
$header-margin-bottom: 0.3125rem;
|
||||
$header-styles: (small: ("h1": ("font-size": 24),
|
||||
"h2": ("font-size": 20),
|
||||
"h3": ("font-size": 19),
|
||||
"h4": ("font-size": 18),
|
||||
"h5": ("font-size": 17),
|
||||
"h6": ("font-size": 16)),
|
||||
medium: ("h1": ("font-size": 48),
|
||||
"h2": ("font-size": 40),
|
||||
"h3": ("font-size": 31),
|
||||
"h4": ("font-size": 25),
|
||||
"h5": ("font-size": 20),
|
||||
"h6": ("font-size": 16)));
|
||||
$header-text-rendering: optimizeLegibility;
|
||||
$small-font-size: 80%;
|
||||
$header-small-font-color: $medium-gray;
|
||||
$paragraph-lineheight: 1.65;
|
||||
$paragraph-margin-bottom: var(--space-small);
|
||||
$paragraph-text-rendering: optimizeLegibility;
|
||||
$code-color: $black;
|
||||
$code-font-family: $font-family-monospace;
|
||||
$code-font-weight: $global-weight-normal;
|
||||
$code-background: $light-gray;
|
||||
$code-border: 1px solid $medium-gray;
|
||||
$code-padding: rem-calc(2 5 1);
|
||||
$anchor-color: $primary-color;
|
||||
$anchor-color-hover: scale-color($anchor-color, $lightness: -14%);
|
||||
$anchor-text-decoration: none;
|
||||
$anchor-text-decoration-hover: none;
|
||||
$hr-width: $global-width;
|
||||
$hr-border: 1px solid $medium-gray;
|
||||
$hr-margin: rem-calc(20) auto;
|
||||
$list-lineheight: $paragraph-lineheight;
|
||||
$list-margin-bottom: $paragraph-margin-bottom;
|
||||
$list-style-type: disc;
|
||||
$list-style-position: outside;
|
||||
$list-side-margin: 0.78125rem;
|
||||
$list-nested-side-margin: 0.78125rem;
|
||||
$defnlist-margin-bottom: 0.6875rem;
|
||||
$defnlist-term-weight: $global-weight-bold;
|
||||
$defnlist-term-margin-bottom: 0.1875rem;
|
||||
$blockquote-color: $dark-gray;
|
||||
$blockquote-padding: rem-calc(9 20 0 19);
|
||||
$blockquote-border: 1px solid $medium-gray;
|
||||
$cite-font-size: rem-calc(13);
|
||||
$cite-color: $dark-gray;
|
||||
$cite-pseudo-content: '\2014 \0020';
|
||||
$keystroke-font: $font-family-monospace;
|
||||
$keystroke-color: $black;
|
||||
$keystroke-background: $light-gray;
|
||||
$keystroke-padding: rem-calc(2 4 0);
|
||||
$keystroke-radius: $global-radius;
|
||||
$abbr-underline: 1px dotted $black;
|
||||
|
||||
// 5. Typography Helpers
|
||||
// ---------------------
|
||||
|
||||
$lead-font-size: $global-font-size * 1.25;
|
||||
$lead-lineheight: 1.6;
|
||||
$subheader-lineheight: 1.4;
|
||||
$subheader-color: $dark-gray;
|
||||
$subheader-font-weight: $global-weight-normal;
|
||||
$subheader-margin-top: 0.125rem;
|
||||
$subheader-margin-bottom: 0.3125rem;
|
||||
$stat-font-size: 1.5625rem;
|
||||
|
||||
// 6. Abide
|
||||
// --------
|
||||
|
||||
$abide-inputs: true;
|
||||
$abide-labels: true;
|
||||
$input-background-invalid: get-color(alert);
|
||||
$form-label-color-invalid: get-color(alert);
|
||||
$input-error-color: get-color(alert);
|
||||
$input-error-font-size: rem-calc(12);
|
||||
$input-error-font-weight: $global-weight-bold;
|
||||
|
||||
// 7. Accordion
|
||||
// ------------
|
||||
|
||||
$accordion-background: $white;
|
||||
$accordion-plusminus: true;
|
||||
$accordion-title-font-size: rem-calc(12);
|
||||
$accordion-item-color: $primary-color;
|
||||
$accordion-item-background-hover: $light-gray;
|
||||
$accordion-item-padding: 0.78125rem 0.625rem;
|
||||
$accordion-content-background: $white;
|
||||
$accordion-content-border: 1px solid $light-gray;
|
||||
$accordion-content-color: $body-font-color;
|
||||
$accordion-content-padding: 0.625rem;
|
||||
|
||||
// 8. Accordion Menu
|
||||
// -----------------
|
||||
|
||||
$accordionmenu-arrows: true;
|
||||
$accordionmenu-arrow-color: $primary-color;
|
||||
$accordionmenu-arrow-size: 6px;
|
||||
|
||||
// 9. Badge
|
||||
// --------
|
||||
|
||||
$badge-background: $primary-color;
|
||||
$badge-color: $white;
|
||||
$badge-color-alt: $black;
|
||||
$badge-palette: $foundation-palette;
|
||||
$badge-padding: var(--space-smaller);
|
||||
$badge-minwidth: 2.1em;
|
||||
$badge-font-size: var(--font-size-nano);
|
||||
|
||||
// 10. Breadcrumbs
|
||||
// ---------------
|
||||
|
||||
$breadcrumbs-margin: 0 0 $global-margin 0;
|
||||
$breadcrumbs-item-font-size: rem-calc(11);
|
||||
$breadcrumbs-item-color: $primary-color;
|
||||
$breadcrumbs-item-color-current: $black;
|
||||
$breadcrumbs-item-color-disabled: $medium-gray;
|
||||
$breadcrumbs-item-margin: 0.46875rem;
|
||||
$breadcrumbs-item-uppercase: true;
|
||||
$breadcrumbs-item-slash: true;
|
||||
|
||||
// 11. Button
|
||||
// ----------
|
||||
|
||||
$button-padding: var(--space-smaller) 1em;
|
||||
$button-margin: 0 0 $global-margin 0;
|
||||
$button-fill: solid;
|
||||
$button-background: $primary-color;
|
||||
$button-background-hover: scale-color($button-background, $lightness: -15%);
|
||||
$button-color: $white;
|
||||
$button-color-alt: $white;
|
||||
$button-radius: var(--border-radius-normal);
|
||||
$button-sizes: (tiny: var(--font-size-micro),
|
||||
small: var(--font-size-mini),
|
||||
default: var(--font-size-small),
|
||||
large: var(--font-size-medium));
|
||||
$button-palette: $foundation-palette;
|
||||
$button-opacity-disabled: 0.4;
|
||||
$button-background-hover-lightness: -20%;
|
||||
$button-hollow-hover-lightness: -50%;
|
||||
$button-transition: background-color 0.25s ease-out,
|
||||
color 0.25s ease-out;
|
||||
|
||||
// 12. Button Group
|
||||
// ----------------
|
||||
|
||||
$buttongroup-margin: 0;
|
||||
$buttongroup-spacing: 0;
|
||||
$buttongroup-child-selector: '.button';
|
||||
$buttongroup-expand-max: 6;
|
||||
$buttongroup-radius-on-each: false;
|
||||
|
||||
// 13. Callout
|
||||
// -----------
|
||||
|
||||
$callout-background: $white;
|
||||
$callout-background-fade: 85%;
|
||||
$callout-border: 1px solid rgba($black, 0.25);
|
||||
$callout-margin: 0 0 0.625rem 0;
|
||||
$callout-padding: 0.625rem;
|
||||
$callout-font-color: $body-font-color;
|
||||
$callout-font-color-alt: $body-background;
|
||||
$callout-radius: $global-radius;
|
||||
$callout-link-tint: 30%;
|
||||
|
||||
// 14. Card
|
||||
// --------
|
||||
|
||||
$card-background: $white;
|
||||
$card-font-color: $body-font-color;
|
||||
$card-divider-background: $light-gray;
|
||||
$card-border: 1px solid var(--color-border);
|
||||
$card-shadow: var(--shadow-small);
|
||||
$card-border-radius: var(--border-radius-normal);
|
||||
$card-padding: var(--space-small);
|
||||
$card-margin: $global-margin;
|
||||
|
||||
// 15. Close Button
|
||||
// ----------------
|
||||
|
||||
$closebutton-position: right top;
|
||||
$closebutton-offset-horizontal: (small: 0.66rem,
|
||||
medium: 1rem);
|
||||
$closebutton-offset-vertical: (small: 0.33em,
|
||||
medium: 0.5rem);
|
||||
$closebutton-size: (small: 1.5em,
|
||||
medium: 2em);
|
||||
$closebutton-lineheight: 1;
|
||||
$closebutton-color: $dark-gray;
|
||||
$closebutton-color-hover: $black;
|
||||
|
||||
// 16. Drilldown
|
||||
// -------------
|
||||
|
||||
$drilldown-transition: transform 0.15s linear;
|
||||
$drilldown-arrows: true;
|
||||
$drilldown-arrow-color: $primary-color;
|
||||
$drilldown-arrow-size: 6px;
|
||||
$drilldown-background: $white;
|
||||
|
||||
// 17. Dropdown
|
||||
// ------------
|
||||
|
||||
$dropdown-padding: 0.625rem;
|
||||
$dropdown-background: $body-background;
|
||||
$dropdown-border: 1px solid $medium-gray;
|
||||
$dropdown-font-size: 0.625rem;
|
||||
$dropdown-width: 300px;
|
||||
$dropdown-radius: $global-radius;
|
||||
$dropdown-sizes: (tiny: 100px,
|
||||
small: 200px,
|
||||
large: 400px);
|
||||
|
||||
// 18. Dropdown Menu
|
||||
// -----------------
|
||||
|
||||
$dropdownmenu-arrows: true;
|
||||
$dropdownmenu-arrow-color: $anchor-color;
|
||||
$dropdownmenu-arrow-size: 6px;
|
||||
$dropdownmenu-min-width: 200px;
|
||||
$dropdownmenu-background: $white;
|
||||
$dropdownmenu-border: 1px solid $medium-gray;
|
||||
|
||||
// 19. Forms
|
||||
// ---------
|
||||
|
||||
$fieldset-border: 1px solid $light-gray;
|
||||
$fieldset-padding: $space-two;
|
||||
$fieldset-margin: $space-one $zero;
|
||||
$legend-padding: rem-calc(0 3);
|
||||
$form-spacing: $space-normal;
|
||||
$helptext-color: $color-body;
|
||||
$helptext-font-size: $font-size-small;
|
||||
$helptext-font-style: italic;
|
||||
$input-prefix-color: $color-body;
|
||||
$input-prefix-background: var(--b-100);
|
||||
$input-prefix-border: 1px solid $color-border;
|
||||
$input-prefix-padding: 0.625rem;
|
||||
$form-label-color: $color-body;
|
||||
$form-label-font-size: rem-calc(14);
|
||||
$form-label-font-weight: $font-weight-medium;
|
||||
$form-label-line-height: 1.8;
|
||||
$select-background: $white;
|
||||
$select-triangle-color: $dark-gray;
|
||||
$select-radius: var(--border-radius-normal);
|
||||
$input-color: $color-body;
|
||||
$input-placeholder-color: $light-gray;
|
||||
$input-font-family: inherit;
|
||||
$input-font-size: $font-size-default;
|
||||
$input-font-weight: $global-weight-normal;
|
||||
$input-background: $white;
|
||||
$input-background-focus: $white;
|
||||
$input-background-disabled: $light-gray;
|
||||
$input-border: 1px solid var(--s-200);
|
||||
$input-border-focus: 1px solid lighten($primary-color, 15%);
|
||||
$input-shadow: 0;
|
||||
$input-shadow-focus: 0;
|
||||
$input-cursor-disabled: not-allowed;
|
||||
$input-transition: border-color 0.25s ease-in-out;
|
||||
$input-number-spinners: true;
|
||||
$input-radius: var(--border-radius-normal);
|
||||
$form-button-radius: var(--border-radius-normal);
|
||||
|
||||
// 20. Label
|
||||
// ---------
|
||||
|
||||
$label-background: $white;
|
||||
$label-color: $black;
|
||||
$label-color-alt: $black;
|
||||
$label-palette: $foundation-palette;
|
||||
$label-font-size: $font-size-mini;
|
||||
$label-padding: $space-smaller $space-small;
|
||||
$label-radius: var(--border-radius-small);
|
||||
|
||||
// 21. Media Object
|
||||
// ----------------
|
||||
|
||||
$mediaobject-margin-bottom: $global-margin;
|
||||
$mediaobject-section-padding: $global-padding;
|
||||
$mediaobject-image-width-stacked: 100%;
|
||||
|
||||
// 22. Menu
|
||||
// --------
|
||||
|
||||
$menu-margin: 0;
|
||||
$menu-margin-nested: $space-medium;
|
||||
$menu-item-padding: $space-slab;
|
||||
$menu-item-color-active: $white;
|
||||
$menu-item-background-active: $color-background;
|
||||
$menu-icon-spacing: 0.15625rem;
|
||||
$menu-item-background-hover: $light-gray;
|
||||
$menu-border: $light-gray;
|
||||
|
||||
// 23. Meter
|
||||
// ---------
|
||||
|
||||
$meter-height: 0.625rem;
|
||||
$meter-radius: $global-radius;
|
||||
$meter-background: $medium-gray;
|
||||
$meter-fill-good: $success-color;
|
||||
$meter-fill-medium: $warning-color;
|
||||
$meter-fill-bad: $alert-color;
|
||||
|
||||
// 24. Off-canvas
|
||||
// --------------
|
||||
|
||||
$offcanvas-sizes: (small: 14.375,
|
||||
medium: 14.375,
|
||||
);
|
||||
$offcanvas-vertical-sizes: (small: 14.375,
|
||||
medium: 14.375,
|
||||
);
|
||||
$offcanvas-background: $light-gray;
|
||||
$offcanvas-shadow: 0 0 10px rgba($black, 0.7);
|
||||
$offcanvas-push-zindex: 1;
|
||||
$offcanvas-overlap-zindex: 10;
|
||||
$offcanvas-reveal-zindex: 1;
|
||||
$offcanvas-transition-length: 0.5s;
|
||||
$offcanvas-transition-timing: ease;
|
||||
$offcanvas-fixed-reveal: true;
|
||||
$offcanvas-exit-background: rgba($white, 0.25);
|
||||
$maincontent-class: 'off-canvas-content';
|
||||
|
||||
// 25. Orbit
|
||||
// ---------
|
||||
|
||||
$orbit-bullet-background: $medium-gray;
|
||||
$orbit-bullet-background-active: $dark-gray;
|
||||
$orbit-bullet-diameter: 0.75rem;
|
||||
$orbit-bullet-margin: 0.0625rem;
|
||||
$orbit-bullet-margin-top: 0.5rem;
|
||||
$orbit-bullet-margin-bottom: 0.5rem;
|
||||
$orbit-caption-background: rgba($black, 0.5);
|
||||
$orbit-caption-padding: 0.625rem;
|
||||
$orbit-control-background-hover: rgba($black, 0.5);
|
||||
$orbit-control-padding: 0.625rem;
|
||||
$orbit-control-zindex: 10;
|
||||
|
||||
// 26. Pagination
|
||||
// --------------
|
||||
|
||||
$pagination-font-size: rem-calc(14);
|
||||
$pagination-margin-bottom: $global-margin;
|
||||
$pagination-item-color: $black;
|
||||
$pagination-item-padding: rem-calc(3 10);
|
||||
$pagination-item-spacing: rem-calc(1);
|
||||
$pagination-radius: $global-radius;
|
||||
$pagination-item-background-hover: $light-gray;
|
||||
$pagination-item-background-current: $primary-color;
|
||||
$pagination-item-color-current: $white;
|
||||
$pagination-item-color-disabled: $medium-gray;
|
||||
$pagination-ellipsis-color: $black;
|
||||
$pagination-mobile-items: false;
|
||||
$pagination-mobile-current-item: false;
|
||||
$pagination-arrows: true;
|
||||
|
||||
// 27. Progress Bar
|
||||
// ----------------
|
||||
|
||||
$progress-height: 0.625rem;
|
||||
$progress-background: $medium-gray;
|
||||
$progress-margin-bottom: $global-margin;
|
||||
$progress-meter-background: $primary-color;
|
||||
$progress-radius: $global-radius;
|
||||
|
||||
// 28. Responsive Embed
|
||||
// --------------------
|
||||
|
||||
$responsive-embed-margin-bottom: rem-calc(16);
|
||||
$responsive-embed-ratios: (default: 4 by 3,
|
||||
widescreen: 16 by 9);
|
||||
|
||||
// 29. Reveal
|
||||
// ----------
|
||||
|
||||
$reveal-background: $white;
|
||||
$reveal-width: 600px;
|
||||
$reveal-max-width: $global-width;
|
||||
$reveal-padding: $global-padding;
|
||||
$reveal-border: 1px solid $medium-gray;
|
||||
$reveal-radius: $global-radius;
|
||||
$reveal-zindex: 1005;
|
||||
$reveal-overlay-background: rgba($black, 0.45);
|
||||
|
||||
// 30. Slider
|
||||
// ----------
|
||||
|
||||
$slider-width-vertical: 0.3125rem;
|
||||
$slider-transition: all 0.2s ease-in-out;
|
||||
$slider-height: 0.3125rem;
|
||||
$slider-background: $light-gray;
|
||||
$slider-fill-background: $medium-gray;
|
||||
$slider-handle-height: 0.875rem;
|
||||
$slider-handle-width: 0.875rem;
|
||||
$slider-handle-background: $primary-color;
|
||||
$slider-opacity-disabled: 0.25;
|
||||
$slider-radius: $global-radius;
|
||||
|
||||
// 31. Switch
|
||||
// ----------
|
||||
|
||||
$switch-background: $light-gray;
|
||||
$switch-background-active: $primary-color;
|
||||
$switch-height: $space-two;
|
||||
$switch-height-tiny: $space-slab;
|
||||
$switch-height-small: $space-normal;
|
||||
$switch-height-large: $space-large;
|
||||
$switch-radius: $space-large;
|
||||
$switch-margin: $global-margin;
|
||||
$switch-paddle-background: $white;
|
||||
$switch-paddle-offset: $space-micro;
|
||||
$switch-paddle-radius: $space-large;
|
||||
$switch-paddle-transition: all 0.15s ease-out;
|
||||
|
||||
// 32. Table
|
||||
// ---------
|
||||
|
||||
$table-background: transparent;
|
||||
$table-color-scale: 5%;
|
||||
$table-border: 1px solid transparent;
|
||||
$table-padding: rem-calc(8 10 10);
|
||||
$table-hover-scale: 2%;
|
||||
$table-row-hover: darken($table-background, $table-hover-scale);
|
||||
$table-row-stripe-hover: darken($table-background,
|
||||
$table-color-scale + $table-hover-scale);
|
||||
$table-is-striped: false;
|
||||
$table-striped-background: smart-scale($table-background, $table-color-scale);
|
||||
$table-stripe: even;
|
||||
$table-head-background: smart-scale($table-background, $table-color-scale / 2);
|
||||
$table-head-row-hover: darken($table-head-background, $table-hover-scale);
|
||||
$table-foot-background: smart-scale($table-background, $table-color-scale);
|
||||
$table-foot-row-hover: darken($table-foot-background, $table-hover-scale);
|
||||
$table-head-font-color: $body-font-color;
|
||||
$table-foot-font-color: $body-font-color;
|
||||
$show-header-for-stacked: false;
|
||||
|
||||
// 33. Tabs
|
||||
// --------
|
||||
|
||||
$tab-margin: 0;
|
||||
|
||||
$tab-background: transparent;
|
||||
$tab-background-active: transparent;
|
||||
$tab-item-font-size: $font-size-small;
|
||||
$tab-item-background-hover: transparent;
|
||||
$tab-item-padding: $space-one $zero;
|
||||
$tab-color: $primary-color;
|
||||
$tab-active-color: $primary-color;
|
||||
$tab-expand-max: 6;
|
||||
$tab-content-background: transparent;
|
||||
$tab-content-border: transparent;
|
||||
$tab-content-color: foreground($tab-background, $primary-color);
|
||||
$tab-content-padding: 0.625rem;
|
||||
|
||||
// 34. Thumbnail
|
||||
// -------------
|
||||
|
||||
$thumbnail-border: solid 4px $white;
|
||||
$thumbnail-margin-bottom: $global-margin;
|
||||
$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2);
|
||||
$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5);
|
||||
$thumbnail-transition: box-shadow 200ms ease-out;
|
||||
$thumbnail-radius: $global-radius;
|
||||
|
||||
// 35. Title Bar
|
||||
// -------------
|
||||
|
||||
$titlebar-background: $black;
|
||||
$titlebar-color: $white;
|
||||
$titlebar-padding: 0.3125rem;
|
||||
$titlebar-text-font-weight: bold;
|
||||
$titlebar-icon-color: $white;
|
||||
$titlebar-icon-color-hover: $medium-gray;
|
||||
$titlebar-icon-spacing: 0.15625rem;
|
||||
|
||||
// 36. Tooltip
|
||||
// -----------
|
||||
|
||||
$has-tip-font-weight: $global-weight-bold;
|
||||
$has-tip-border-bottom: dotted 1px $dark-gray;
|
||||
$tooltip-background-color: $black;
|
||||
$tooltip-color: $white;
|
||||
$tooltip-padding: 0.46875rem;
|
||||
$tooltip-font-size: $font-size-mini;
|
||||
$tooltip-pip-width: 0.46875rem;
|
||||
$tooltip-pip-height: $tooltip-pip-width * 0.866;
|
||||
$tooltip-radius: $global-radius;
|
||||
|
||||
// 37. Top Bar
|
||||
// -----------
|
||||
|
||||
$topbar-padding: 0.3125;
|
||||
$topbar-background: $light-gray;
|
||||
$topbar-submenu-background: $topbar-background;
|
||||
$topbar-title-spacing: 0.3125 0.625rem 0.3125 0;
|
||||
$topbar-input-width: 200px;
|
||||
$topbar-unstack-breakpoint: medium;
|
||||
|
||||
|
||||
// Internal variable that contains the flex justifying options
|
||||
$-zf-flex-justify: -zf-flex-justify($global-text-direction);
|
||||
|
||||
$menu-items-padding: $space-one;
|
||||
$xy-grid: false;
|
||||
@@ -1,64 +1,22 @@
|
||||
.bg-light {
|
||||
@apply bg-slate-25 dark:bg-slate-800;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
@include flex-align(center, middle);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bottom-space-fix {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
@include full-height();
|
||||
}
|
||||
|
||||
// loader class
|
||||
.spinner {
|
||||
@include color-spinner();
|
||||
display: inline-block;
|
||||
height: $space-medium;
|
||||
padding: $zero $space-medium;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
width: $space-medium;
|
||||
@apply inline-block h-6 py-0 px-6 relative align-middle w-6;
|
||||
|
||||
&.message {
|
||||
@include normal-shadow;
|
||||
background: $color-white;
|
||||
border-radius: $space-large;
|
||||
left: 0;
|
||||
margin: $space-slab auto;
|
||||
padding: $space-normal;
|
||||
top: 0;
|
||||
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0;
|
||||
|
||||
&::before {
|
||||
margin-left: -$space-slab;
|
||||
margin-top: -$space-slab;
|
||||
@apply -ml-3 -mt-3;
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: $space-normal;
|
||||
width: $space-normal;
|
||||
@apply h-4 w-4;
|
||||
|
||||
&::before {
|
||||
height: $space-normal;
|
||||
margin-top: -$space-small;
|
||||
width: $space-normal;
|
||||
@apply h-4 -mt-2 w-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.justify-space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
// scss-lint:disable SpaceAfterPropertyColon
|
||||
@import 'shared/assets/fonts/inter';
|
||||
// Inter,
|
||||
html,
|
||||
body {
|
||||
font-family:
|
||||
'PlusJakarta',
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Tahoma,
|
||||
Arial,
|
||||
sans-serif !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
|
||||
@@ -131,37 +131,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.search-header--wrap {
|
||||
.search--input {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.layout-switch__container {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Basic filter dropdown
|
||||
.basic-filter {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
// Card label
|
||||
.label-container {
|
||||
.label {
|
||||
margin-left: var(--space-smaller);
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary sidebar toggle button
|
||||
.toggle-sidebar {
|
||||
margin-left: 0;
|
||||
margin-right: var(--space-minus-small);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
// Bulk actions
|
||||
.bulk-action__container {
|
||||
.triangle {
|
||||
@@ -202,22 +177,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Notification panel
|
||||
.notification-wrap {
|
||||
left: 0;
|
||||
right: var(--space-jumbo);
|
||||
|
||||
.action-button {
|
||||
margin-left: var(--space-small);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.notification-content--wrap {
|
||||
margin-left: 0;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
// Help center
|
||||
.article-container .row--article-block {
|
||||
td:last-child {
|
||||
@@ -324,10 +283,6 @@
|
||||
|
||||
// Other changes
|
||||
|
||||
.account-selector--wrap {
|
||||
direction: initial;
|
||||
}
|
||||
|
||||
.colorpicker--chrome {
|
||||
direction: initial;
|
||||
}
|
||||
@@ -347,9 +302,4 @@
|
||||
.contact--form .input-group {
|
||||
direction: initial;
|
||||
}
|
||||
|
||||
// scss-lint:disable QualifyingElement
|
||||
.dropdown-menu--header > span.title {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
.page-title {
|
||||
font-size: $font-size-big;
|
||||
}
|
||||
|
||||
.page-sub-title {
|
||||
font-size: $font-size-large;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
font-size: $font-size-medium;
|
||||
}
|
||||
|
||||
.sub-block-title {
|
||||
font-size: $font-size-default;
|
||||
}
|
||||
|
||||
.text-block-title {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--s-300);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-size-small;
|
||||
word-spacing: .12em;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
.margin-bottom-small {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
.margin-right-smaller {
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
|
||||
.margin-left-minus-slab {
|
||||
margin-left: var(--space-minus-slab);
|
||||
}
|
||||
|
||||
.margin-right-minus-slab {
|
||||
margin-right: var(--space-minus-slab);
|
||||
}
|
||||
|
||||
.fs-small {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.fs-default {
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
|
||||
.fw-medium {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.p-normal {
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.overflow-scroll {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.border-right {
|
||||
@apply border-r border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
.border-left {
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.flex-align-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
@@ -15,63 +15,372 @@
|
||||
@import 'variables';
|
||||
|
||||
@import 'mixins';
|
||||
@import 'foundation-settings';
|
||||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
@import 'date-picker';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
|
||||
@include foundation-everything($flex: true);
|
||||
|
||||
@include foundation-prototype-text-utilities;
|
||||
@include foundation-prototype-text-transformation;
|
||||
@include foundation-prototype-text-decoration;
|
||||
@include foundation-prototype-font-styling;
|
||||
@include foundation-prototype-list-style-type;
|
||||
@include foundation-prototype-rounded;
|
||||
@include foundation-prototype-bordered;
|
||||
@include foundation-prototype-shadow;
|
||||
@include foundation-prototype-separator;
|
||||
@include foundation-prototype-overflow;
|
||||
@include foundation-prototype-display;
|
||||
@include foundation-prototype-position;
|
||||
@include foundation-prototype-border-box;
|
||||
@include foundation-prototype-border-none;
|
||||
@include foundation-prototype-sizing;
|
||||
@include foundation-prototype-spacing;
|
||||
|
||||
@import 'typography';
|
||||
@import 'layout';
|
||||
@import 'animations';
|
||||
@import 'foundation-custom';
|
||||
@import 'rtl';
|
||||
|
||||
@import 'widgets/base';
|
||||
@import 'widgets/buttons';
|
||||
@import 'widgets/conv-header';
|
||||
@import 'widgets/conversation-card';
|
||||
@import 'widgets/conversation-view';
|
||||
@import 'widgets/forms';
|
||||
@import 'widgets/login';
|
||||
@import 'widgets/modal';
|
||||
@import 'widgets/reply-box';
|
||||
@import 'widgets/report';
|
||||
@import 'widgets/search-box';
|
||||
@import 'widgets/sidemenu';
|
||||
@import 'widgets/snackbar';
|
||||
@import 'widgets/states';
|
||||
@import 'widgets/status-bar';
|
||||
@import 'widgets/tabs';
|
||||
@import 'widgets/woot-tables';
|
||||
|
||||
@import 'views/settings/inbox';
|
||||
@import 'views/settings/integrations';
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
@import 'plugins/dropdown';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
@import 'utility-helpers';
|
||||
|
||||
.tooltip {
|
||||
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900;
|
||||
}
|
||||
|
||||
.hide {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
// scss-lint:disable PropertySortOrder
|
||||
:root {
|
||||
--color-amber-25: 254 253 251;
|
||||
--color-amber-50: 255 249 237;
|
||||
--color-amber-75: 255 243 208;
|
||||
--color-amber-100: 255 236 183;
|
||||
--color-amber-200: 255 224 161;
|
||||
--color-amber-300: 245 208 140;
|
||||
--color-amber-400: 228 187 120;
|
||||
--color-amber-500: 214 163 92;
|
||||
--color-amber-600: 214 163 92;
|
||||
--color-amber-700: 255 186 26;
|
||||
--color-amber-800: 145 89 48;
|
||||
--color-amber-900: 79 52 34;
|
||||
|
||||
--color-ash-100: 235 235 239;
|
||||
--color-ash-200: 228 228 233;
|
||||
--color-ash-25: 252 252 253;
|
||||
--color-ash-300: 221 221 227;
|
||||
--color-ash-400: 211 212 219;
|
||||
--color-ash-50: 249 249 251;
|
||||
--color-ash-500: 185 187 198;
|
||||
--color-ash-600: 139 141 152;
|
||||
--color-ash-700: 126 128 138;
|
||||
--color-ash-75: 242 242 245;
|
||||
--color-ash-800: 96 100 108;
|
||||
--color-ash-900: 28 32 36;
|
||||
|
||||
--color-primary-25: 251 253 255;
|
||||
--color-primary-50: 245 249 255;
|
||||
--color-primary-75: 233 243 255;
|
||||
--color-primary-100: 218 236 255;
|
||||
--color-primary-200: 201 226 255;
|
||||
--color-primary-300: 181 213 255;
|
||||
--color-primary-400: 155 195 252;
|
||||
--color-primary-500: 117 171 247;
|
||||
--color-primary-600: 39 129 246;
|
||||
--color-primary-700: 16 115 233;
|
||||
--color-primary-800: 8 109 224;
|
||||
--color-primary-900: 11 50 101;
|
||||
|
||||
--color-ruby-100: 255 220 225;
|
||||
--color-ruby-200: 255 206 214;
|
||||
--color-ruby-25: 255 252 253;
|
||||
--color-ruby-300: 248 191 200;
|
||||
--color-ruby-400: 239 172 184;
|
||||
--color-ruby-50: 255 247 248;
|
||||
--color-ruby-500: 229 146 163;
|
||||
--color-ruby-600: 229 70 102;
|
||||
--color-ruby-700: 220 59 93;
|
||||
--color-ruby-75: 254 234 237;
|
||||
--color-ruby-800: 202 36 77;
|
||||
--color-ruby-900: 100 23 43;
|
||||
|
||||
--color-teal-100: 224 248 243;
|
||||
--color-teal-200: 204 243 234;
|
||||
--color-teal-25: 250 254 253;
|
||||
--color-teal-300: 184 234 224;
|
||||
--color-teal-400: 161 222 210;
|
||||
--color-teal-50: 243 251 249;
|
||||
--color-teal-500: 83 185 171;
|
||||
--color-teal-600: 18 165 148;
|
||||
--color-teal-700: 13 155 138;
|
||||
--color-teal-75: 236 249 255;
|
||||
--color-teal-800: 0 133 115;
|
||||
--color-teal-900: 13 61 56;
|
||||
|
||||
--color-green-25: 251 254 252;
|
||||
--color-green-50: 244 251 246;
|
||||
--color-green-75: 230 246 235;
|
||||
--color-green-100: 214 241 223;
|
||||
--color-green-200: 196 232 209;
|
||||
--color-green-300: 173 221 192;
|
||||
--color-green-400: 142 206 170;
|
||||
--color-green-500: 91 185 139;
|
||||
--color-green-600: 48 164 108;
|
||||
--color-green-700: 43 154 102;
|
||||
--color-green-800: 33 131 88;
|
||||
--color-green-900: 25 59 45;
|
||||
|
||||
--color-mint-25: 249 254 253;
|
||||
--color-mint-50: 242 251 249;
|
||||
--color-mint-75: 221 249 242;
|
||||
--color-mint-100: 200 244 233;
|
||||
--color-mint-200: 179 236 222;
|
||||
--color-mint-300: 156 224 208;
|
||||
--color-mint-400: 126 207 189;
|
||||
--color-mint-500: 76 187 165;
|
||||
--color-mint-600: 134 234 212;
|
||||
--color-mint-700: 125 224 203;
|
||||
--color-mint-800: 2 120 100;
|
||||
--color-mint-900: 22 67 60;
|
||||
|
||||
--color-sky-25: 249 254 255;
|
||||
--color-sky-50: 241 250 253;
|
||||
--color-sky-75: 225 246 253;
|
||||
--color-sky-100: 209 240 250;
|
||||
--color-sky-200: 190 231 245;
|
||||
--color-sky-300: 169 218 237;
|
||||
--color-sky-400: 141 202 227;
|
||||
--color-sky-500: 96 179 215;
|
||||
--color-sky-600: 124 226 254;
|
||||
--color-sky-700: 116 218 248;
|
||||
--color-sky-800: 0 116 158;
|
||||
--color-sky-900: 29 62 86;
|
||||
|
||||
--color-indigo-25: 253 253 254;
|
||||
--color-indigo-50: 247 249 255;
|
||||
--color-indigo-75: 237 242 254;
|
||||
--color-indigo-100: 225 233 255;
|
||||
--color-indigo-200: 210 222 255;
|
||||
--color-indigo-300: 193 208 255;
|
||||
--color-indigo-400: 171 189 249;
|
||||
--color-indigo-500: 141 164 239;
|
||||
--color-indigo-600: 62 99 221;
|
||||
--color-indigo-700: 51 88 212;
|
||||
--color-indigo-800: 58 91 199;
|
||||
--color-indigo-900: 31 45 92;
|
||||
|
||||
--color-iris-25: 253 253 255;
|
||||
--color-iris-50: 248 248 255;
|
||||
--color-iris-75: 240 241 254;
|
||||
--color-iris-100: 230 231 255;
|
||||
--color-iris-200: 218 220 255;
|
||||
--color-iris-300: 203 205 255;
|
||||
--color-iris-400: 184 186 248;
|
||||
--color-iris-500: 155 158 240;
|
||||
--color-iris-600: 91 91 214;
|
||||
--color-iris-700: 81 81 205;
|
||||
--color-iris-800: 87 83 198;
|
||||
--color-iris-900: 39 41 98;
|
||||
|
||||
--color-violet-25: 253 252 254;
|
||||
--color-violet-50: 250 248 255;
|
||||
--color-violet-75: 244 240 254;
|
||||
--color-violet-100: 235 228 255;
|
||||
--color-violet-200: 225 217 255;
|
||||
--color-violet-300: 212 202 254;
|
||||
--color-violet-400: 194 181 245;
|
||||
--color-violet-500: 170 153 236;
|
||||
--color-violet-600: 110 86 207;
|
||||
--color-violet-700: 101 77 196;
|
||||
--color-violet-800: 101 80 185;
|
||||
--color-violet-900: 47 38 95;
|
||||
|
||||
--color-pink-25: 255 252 254;
|
||||
--color-pink-50: 254 247 251;
|
||||
--color-pink-75: 254 233 245;
|
||||
--color-pink-100: 251 220 239;
|
||||
--color-pink-200: 246 206 231;
|
||||
--color-pink-300: 239 191 221;
|
||||
--color-pink-400: 231 172 208;
|
||||
--color-pink-500: 221 147 194;
|
||||
--color-pink-600: 214 64 159;
|
||||
--color-pink-700: 207 56 151;
|
||||
--color-pink-800: 194 41 138;
|
||||
--color-pink-900: 101 18 73;
|
||||
|
||||
--color-orange-25: 254 252 251;
|
||||
--color-orange-50: 255 247 237;
|
||||
--color-orange-75: 255 239 214;
|
||||
--color-orange-100: 255 223 181;
|
||||
--color-orange-200: 255 209 154;
|
||||
--color-orange-300: 255 193 130;
|
||||
--color-orange-400: 245 174 115;
|
||||
--color-orange-500: 236 148 85;
|
||||
--color-orange-600: 247 107 21;
|
||||
--color-orange-700: 239 95 0;
|
||||
--color-orange-800: 204 78 0;
|
||||
--color-orange-900: 88 45 29;
|
||||
}
|
||||
// scss-lint:disable QualifyingElement
|
||||
body.dark {
|
||||
--color-amber-25: 31 19 0;
|
||||
--color-amber-50: 37 24 4;
|
||||
--color-amber-75: 48 32 11;
|
||||
--color-amber-100: 57 39 15;
|
||||
--color-amber-200: 67 46 18;
|
||||
--color-amber-300: 83 57 22;
|
||||
--color-amber-400: 111 77 29;
|
||||
--color-amber-500: 169 118 42;
|
||||
--color-amber-600: 169 118 42;
|
||||
--color-amber-700: 255 203 71;
|
||||
--color-amber-800: 255 204 77;
|
||||
--color-amber-900: 255 231 179;
|
||||
|
||||
--color-ash-100: 46 48 53;
|
||||
--color-ash-200: 53 55 60;
|
||||
--color-ash-25: 24 24 26;
|
||||
--color-ash-300: 60 63 68;
|
||||
--color-ash-400: 70 75 80;
|
||||
--color-ash-50: 27 27 31;
|
||||
--color-ash-500: 90 97 101;
|
||||
--color-ash-600: 105 110 119;
|
||||
--color-ash-700: 120 127 133;
|
||||
--color-ash-75: 39 40 45;
|
||||
--color-ash-800: 173 177 184;
|
||||
--color-ash-900: 237 238 240;
|
||||
|
||||
--color-primary-25: 10 17 28;
|
||||
--color-primary-50: 15 24 38;
|
||||
--color-primary-75: 15 39 72;
|
||||
--color-primary-100: 10 49 99;
|
||||
--color-primary-200: 18 61 117;
|
||||
--color-primary-300: 29 74 134;
|
||||
--color-primary-400: 40 89 156;
|
||||
--color-primary-500: 48 106 186;
|
||||
--color-primary-600: 39 129 246;
|
||||
--color-primary-700: 21 116 231;
|
||||
--color-primary-800: 126 182 255;
|
||||
--color-primary-900: 205 227 255;
|
||||
|
||||
--color-ruby-100: 78 19 37;
|
||||
--color-ruby-200: 94 26 46;
|
||||
--color-ruby-25: 25 17 19;
|
||||
--color-ruby-300: 111 37 57;
|
||||
--color-ruby-400: 136 52 71;
|
||||
--color-ruby-50: 30 21 23;
|
||||
--color-ruby-500: 179 68 90;
|
||||
--color-ruby-600: 229 70 102;
|
||||
--color-ruby-700: 236 90 114;
|
||||
--color-ruby-75: 58 20 30;
|
||||
--color-ruby-800: 255 148 157;
|
||||
--color-ruby-900: 254 210 225;
|
||||
|
||||
--color-teal-100: 2 59 55;
|
||||
--color-teal-200: 8 72 67;
|
||||
--color-teal-25: 13 21 20;
|
||||
--color-teal-300: 28 105 97;
|
||||
--color-teal-400: 28 105 97;
|
||||
--color-teal-50: 17 28 27;
|
||||
--color-teal-500: 32 126 115;
|
||||
--color-teal-600: 41 163 131;
|
||||
--color-teal-700: 14 179 158;
|
||||
--color-teal-75: 13 45 42;
|
||||
--color-teal-800: 11 216 182;
|
||||
--color-teal-900: 173 240 221;
|
||||
|
||||
--color-green-25: 14 21 18;
|
||||
--color-green-50: 18 27 23;
|
||||
--color-green-75: 19 45 33;
|
||||
--color-green-100: 17 59 41;
|
||||
--color-green-200: 23 73 51;
|
||||
--color-green-300: 32 87 62;
|
||||
--color-green-400: 40 104 74;
|
||||
--color-green-500: 47 124 87;
|
||||
--color-green-600: 48 164 108;
|
||||
--color-green-700: 51 176 116;
|
||||
--color-green-800: 61 214 140;
|
||||
--color-green-900: 177 241 203;
|
||||
|
||||
--color-mint-25: 14 21 21;
|
||||
--color-mint-50: 15 27 27;
|
||||
--color-mint-75: 9 44 43;
|
||||
--color-mint-100: 0 58 56;
|
||||
--color-mint-200: 0 71 68;
|
||||
--color-mint-300: 16 86 80;
|
||||
--color-mint-400: 30 104 95;
|
||||
--color-mint-500: 39 127 112;
|
||||
--color-mint-600: 134 234 212;
|
||||
--color-mint-700: 168 245 229;
|
||||
--color-mint-800: 88 213 186;
|
||||
--color-mint-900: 196 245 225;
|
||||
|
||||
--color-sky-25: 14 21 21;
|
||||
--color-sky-50: 15 27 27;
|
||||
--color-sky-75: 9 44 43;
|
||||
--color-sky-100: 0 58 56;
|
||||
--color-sky-200: 0 71 68;
|
||||
--color-sky-300: 16 86 80;
|
||||
--color-sky-400: 30 104 95;
|
||||
--color-sky-500: 39 127 112;
|
||||
--color-sky-600: 134 234 212;
|
||||
--color-sky-700: 168 245 229;
|
||||
--color-sky-800: 88 213 186;
|
||||
--color-sky-900: 196 245 225;
|
||||
|
||||
--color-indigo-25: 17 19 31;
|
||||
--color-indigo-50: 20 23 38;
|
||||
--color-indigo-75: 24 36 73;
|
||||
--color-indigo-100: 29 46 98;
|
||||
--color-indigo-200: 37 57 116;
|
||||
--color-indigo-300: 48 67 132;
|
||||
--color-indigo-400: 58 79 151;
|
||||
--color-indigo-500: 67 93 177;
|
||||
--color-indigo-600: 62 99 221;
|
||||
--color-indigo-700: 84 114 228;
|
||||
--color-indigo-800: 158 177 255;
|
||||
--color-indigo-900: 214 225 255;
|
||||
|
||||
--color-iris-25: 19 19 30;
|
||||
--color-iris-50: 23 22 37;
|
||||
--color-iris-75: 32 34 72;
|
||||
--color-iris-100: 38 42 101;
|
||||
--color-iris-200: 48 51 116;
|
||||
--color-iris-300: 61 62 130;
|
||||
--color-iris-400: 74 74 149;
|
||||
--color-iris-500: 89 88 177;
|
||||
--color-iris-600: 91 91 214;
|
||||
--color-iris-700: 110 106 222;
|
||||
--color-iris-800: 177 169 255;
|
||||
--color-iris-900: 224 223 254;
|
||||
|
||||
--color-violet-25: 20 18 31;
|
||||
--color-violet-50: 27 21 37;
|
||||
--color-violet-75: 41 31 67;
|
||||
--color-violet-100: 51 37 91;
|
||||
--color-violet-200: 60 46 105;
|
||||
--color-violet-300: 71 56 118;
|
||||
--color-violet-400: 86 70 139;
|
||||
--color-violet-500: 105 88 173;
|
||||
--color-violet-600: 110 86 207;
|
||||
--color-violet-700: 125 102 217;
|
||||
--color-violet-800: 186 167 255;
|
||||
--color-violet-900: 226 221 254;
|
||||
|
||||
--color-pink-25: 25 17 23;
|
||||
--color-pink-50: 33 18 29;
|
||||
--color-pink-75: 55 23 47;
|
||||
--color-pink-100: 75 20 61;
|
||||
--color-pink-200: 89 28 71;
|
||||
--color-pink-300: 105 41 85;
|
||||
--color-pink-400: 131 56 105;
|
||||
--color-pink-500: 168 72 133;
|
||||
--color-pink-600: 214 64 159;
|
||||
--color-pink-700: 222 81 168;
|
||||
--color-pink-800: 255 141 204;
|
||||
--color-pink-900: 253 209 234;
|
||||
--color-orange-25: 23 18 14;
|
||||
--color-orange-50: 30 22 15;
|
||||
--color-orange-75: 51 30 11;
|
||||
--color-orange-100: 70 33 0;
|
||||
--color-orange-200: 86 40 0;
|
||||
--color-orange-300: 102 53 12;
|
||||
--color-orange-400: 126 69 29;
|
||||
--color-orange-500: 163 88 41;
|
||||
--color-orange-600: 247 107 21;
|
||||
--color-orange-700: 255 128 31;
|
||||
--color-orange-800: 255 160 87;
|
||||
--color-orange-900: 255 224 194;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,7 @@
|
||||
.dropdown-pane {
|
||||
@include elegant-card;
|
||||
@include border-light;
|
||||
box-sizing: content-box;
|
||||
padding: var(--space-small);
|
||||
width: fit-content;
|
||||
z-index: var(--z-index-very-high);
|
||||
@apply border rounded-lg hidden relative invisible shadow-lg border-slate-25 dark:border-slate-700 box-content p-2 w-fit z-[9999];
|
||||
|
||||
&.dropdown-pane--open {
|
||||
@apply bg-white dark:bg-slate-800;
|
||||
display: block;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.dropdowm--bottom {
|
||||
&::before {
|
||||
@include arrow(top, var(--color-border-light), 14px);
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: -14px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@include arrow(top, $color-white, var(--space-slab));
|
||||
position: absolute;
|
||||
right: var(--space-small);
|
||||
top: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.dropdowm--top {
|
||||
&::before {
|
||||
@include arrow(bottom, var(--color-border-light), 14px);
|
||||
bottom: -14px;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@include arrow(bottom, $color-white, var(--space-slab));
|
||||
bottom: -12px;
|
||||
position: absolute;
|
||||
right: var(--space-small);
|
||||
}
|
||||
@apply bg-white absolute dark:bg-slate-800 block visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,10 @@
|
||||
|
||||
.multiselect__tags {
|
||||
@apply bg-white dark:bg-slate-900 border border-solid border-slate-200 dark:border-slate-600 m-0 min-h-[2.875rem] pt-0;
|
||||
|
||||
input {
|
||||
@apply border-0 border-none;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
@@ -149,6 +153,28 @@
|
||||
}
|
||||
|
||||
.multiselect-wrap--small {
|
||||
// To be removed one SLA reports date picker is created
|
||||
&.tiny {
|
||||
.multiselect.no-margin {
|
||||
@apply min-h-[32px];
|
||||
}
|
||||
|
||||
.multiselect__select {
|
||||
@apply min-h-[32px] h-8;
|
||||
|
||||
&::before {
|
||||
@apply top-[60%];
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
@apply min-h-[32px] max-h-[32px];
|
||||
|
||||
.multiselect__single {
|
||||
@apply pt-1 pb-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
@@ -180,7 +206,6 @@
|
||||
.multiselect--disabled .multiselect__select {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.multiselect-wrap--medium {
|
||||
|
||||
@@ -13,20 +13,14 @@
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
||||
@import 'mixins';
|
||||
@import 'foundation-settings';
|
||||
@import 'helper-classes';
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
|
||||
@include foundation-prototype-spacing;
|
||||
@include foundation-everything($flex: true);
|
||||
|
||||
@import 'typography';
|
||||
@import 'layout';
|
||||
@import 'animations';
|
||||
|
||||
@import 'foundation-custom';
|
||||
@import 'widgets/buttons';
|
||||
@import 'widgets/forms';
|
||||
@import 'widgets/base';
|
||||
|
||||
@import 'plugins/multiselect';
|
||||
|
||||
@@ -36,7 +30,6 @@
|
||||
@import 'tailwindcss/utilities';
|
||||
@import 'widget/assets/scss/utilities';
|
||||
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: 'PlusJakarta', sans-serif;
|
||||
|
||||
@@ -1,112 +1 @@
|
||||
.settings {
|
||||
@apply overflow-auto;
|
||||
}
|
||||
|
||||
.wizard-box {
|
||||
.item {
|
||||
@apply cursor-pointer py-4 pr-4 pl-6 relative;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
@apply bg-slate-75 dark:bg-slate-600 content-[''] h-full absolute top-5 w-0.5;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@apply h-4 top-0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::before {
|
||||
@apply h-0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::after {
|
||||
@apply h-0;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
h3 {
|
||||
@apply text-woot-500 dark:text-woot-500;
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.over {
|
||||
&::after {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
|
||||
& + .item {
|
||||
&::before {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-slate-800 dark:text-slate-100 text-base pl-6;
|
||||
}
|
||||
|
||||
.completed {
|
||||
@apply text-green-500 dark:text-green-500 ml-1;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-slate-600 dark:text-slate-300 text-sm m-0 pl-6;
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-[999] absolute text-center text-white dark:text-white text-xxs top-5;
|
||||
|
||||
i {
|
||||
@apply text-xxs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wizard-body {
|
||||
@apply border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6;
|
||||
|
||||
&.height-auto {
|
||||
@apply h-auto;
|
||||
}
|
||||
}
|
||||
|
||||
.settings--content {
|
||||
@apply my-2 mx-8;
|
||||
|
||||
.title {
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
.code {
|
||||
@apply bg-slate-50 dark:bg-slate-800 overflow-auto p-2.5 whitespace-nowrap;
|
||||
|
||||
code {
|
||||
@apply bg-transparent border-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-init {
|
||||
@apply pt-[30%] text-center;
|
||||
|
||||
p {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
> a > img {
|
||||
@apply w-60;
|
||||
}
|
||||
}
|
||||
// to be removed
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// scss-lint:disable QualifyingElement
|
||||
|
||||
// Base typography
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-medium text-slate-800 dark:text-slate-50;
|
||||
}
|
||||
|
||||
p {
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
@apply mb-2 leading-[1.65] text-sm;
|
||||
|
||||
a {
|
||||
@apply text-woot-500 dark:text-woot-500 cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply clear-both max-w-full h-0 my-5 mx-0 border-slate-300 dark:border-slate-600;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol,
|
||||
dl {
|
||||
@apply mb-2 list-disc list-outside leading-[1.65];
|
||||
}
|
||||
|
||||
// Form elements
|
||||
label {
|
||||
@apply text-slate-800 dark:text-slate-200 block m-0 leading-7 text-sm font-medium;
|
||||
|
||||
&.error {
|
||||
input {
|
||||
@apply mb-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrap,
|
||||
.help-text {
|
||||
@apply text-slate-800 dark:text-slate-100 text-sm font-medium;
|
||||
|
||||
.help-text {
|
||||
@apply font-normal text-slate-600 dark:text-slate-400;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus outline removal
|
||||
.button,
|
||||
textarea,
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Inputs
|
||||
input[type='text']:not(.reset-base),
|
||||
input[type='number']:not(.reset-base),
|
||||
input[type='password']:not(.reset-base),
|
||||
input[type='date']:not(.reset-base),
|
||||
input[type='email']:not(.reset-base),
|
||||
input[type='url']:not(.reset-base) {
|
||||
@apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-10 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
|
||||
|
||||
&[disabled] {
|
||||
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
@apply bg-white dark:bg-slate-800 leading-[1.15] mb-4;
|
||||
}
|
||||
|
||||
// Select
|
||||
select {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28110, 111, 115%29'></polygon></svg>");
|
||||
background-position: right -1rem center;
|
||||
background-size: 9px 6px;
|
||||
@apply h-10 mx-0 mt-0 mb-4 bg-origin-content focus-visible:outline-none bg-no-repeat py-2 pr-6 pl-2 rounded-md w-full text-base font-normal appearance-none transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
|
||||
}
|
||||
|
||||
// Textarea
|
||||
textarea {
|
||||
@apply block box-border w-full transition-colors focus:border-woot-500 dark:focus:border-woot-600 duration-[0.25s] ease-[ease-in-out] h-16 appearance-none mx-0 mt-0 mb-4 p-2 rounded-md text-base font-normal bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border border-solid border-slate-200 dark:border-slate-600;
|
||||
|
||||
&[disabled] {
|
||||
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling
|
||||
.has-multi-select-error {
|
||||
div.multiselect {
|
||||
@apply mb-1;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
input,
|
||||
input:not([type]),
|
||||
textarea,
|
||||
select,
|
||||
.multiselect > .multiselect__tags,
|
||||
.multiselect:not(.no-margin) {
|
||||
@apply border border-solid border-red-400 dark:border-red-400 mb-1;
|
||||
}
|
||||
|
||||
.message {
|
||||
@apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group.small {
|
||||
input {
|
||||
@apply text-sm h-8;
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply border-red-400 dark:border-red-400;
|
||||
}
|
||||
}
|
||||
|
||||
// Code styling
|
||||
code {
|
||||
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
|
||||
'"Liberation Mono"', '"Courier New"', 'monospace';
|
||||
@apply text-xs border-0;
|
||||
|
||||
&.hljs {
|
||||
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50 rounded-lg p-5;
|
||||
|
||||
.hljs-number,
|
||||
.hljs-string {
|
||||
@apply text-red-800 dark:text-red-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,40 @@
|
||||
// scss-lint:disable SpaceAfterPropertyColon
|
||||
// scss-lint:disable MergeableSelector
|
||||
button {
|
||||
font-family: inherit;
|
||||
transition:
|
||||
background-color 0.25s ease-out,
|
||||
color 0.25s ease-out;
|
||||
@apply inline-block items-center mb-0 text-center align-middle cursor-pointer text-sm mt-0 mx-0 py-1 px-2.5 border border-solid border-transparent dark:border-transparent rounded-[0.3125rem];
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
@apply opacity-40 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
@apply mb-0 flex flex-nowrap items-stretch;
|
||||
|
||||
.button {
|
||||
flex: 0 0 auto;
|
||||
@apply m-0 text-sm rounded-none first:rounded-tl-[0.3125rem] first:rounded-bl-[0.3125rem] last:rounded-tr-[0.3125rem] last:rounded-br-[0.3125rem] rtl:space-x-reverse;
|
||||
}
|
||||
|
||||
.button--only-icon {
|
||||
@apply w-10 justify-center pl-0 pr-0;
|
||||
}
|
||||
}
|
||||
|
||||
.back-button {
|
||||
@apply m-0;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply items-center inline-flex h-10 mb-0 gap-2;
|
||||
@apply items-center bg-woot-500 dark:bg-woot-500 px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium;
|
||||
|
||||
.button__content {
|
||||
@apply w-full;
|
||||
@apply w-full whitespace-nowrap overflow-hidden text-ellipsis;
|
||||
|
||||
img,
|
||||
svg {
|
||||
@@ -10,12 +42,61 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-woot-600 dark:bg-woot-600;
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
@apply opacity-40 cursor-not-allowed;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply bg-[#44ce4b] dark:bg-[#44ce4b] text-white dark:text-white;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
@apply bg-slate-700 dark:bg-slate-600 text-white dark:text-white;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
@apply bg-woot-500 dark:bg-woot-500 text-white dark:text-white;
|
||||
}
|
||||
|
||||
&.clear {
|
||||
@apply text-woot-500 dark:text-woot-500 bg-transparent dark:bg-transparent;
|
||||
}
|
||||
|
||||
&.alert {
|
||||
@apply bg-red-500 dark:bg-red-500 text-white dark:text-white;
|
||||
|
||||
&.clear {
|
||||
@apply bg-transparent dark:bg-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@apply bg-[#ffc532] dark:bg-[#ffc532] text-white dark:text-white;
|
||||
|
||||
&.clear {
|
||||
@apply bg-transparent dark:bg-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.tiny {
|
||||
@apply h-6 text-[10px];
|
||||
}
|
||||
|
||||
&.small {
|
||||
@apply h-8 text-xs;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
@apply px-2 py-0;
|
||||
}
|
||||
|
||||
// @TODDO - Remove after moving all buttons to woot-button
|
||||
.icon + .button__content {
|
||||
.icon+.button__content {
|
||||
@apply w-auto;
|
||||
}
|
||||
|
||||
@@ -34,10 +115,10 @@
|
||||
}
|
||||
|
||||
&.hollow {
|
||||
@apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
|
||||
@apply border border-woot-500 bg-transparent dark:bg-transparent dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
|
||||
|
||||
&.secondary {
|
||||
@apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
|
||||
@apply text-slate-700 border-slate-100 dark:border-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
|
||||
}
|
||||
|
||||
&.success {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// File to be removed
|
||||
@@ -1,16 +0,0 @@
|
||||
@keyframes left-shift-animation {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(1px);
|
||||
}
|
||||
}
|
||||
|
||||
.conversation {
|
||||
&.active {
|
||||
animation: left-shift-animation 0.25s $swift-ease-out-function;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@
|
||||
@apply rounded-r-lg rounded-l mr-auto break-words;
|
||||
|
||||
&:not(.is-unsupported) {
|
||||
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50
|
||||
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50;
|
||||
}
|
||||
|
||||
&.is-image {
|
||||
@@ -91,7 +91,7 @@
|
||||
}
|
||||
|
||||
.file {
|
||||
.text-block-title {
|
||||
.attachment-name {
|
||||
@apply text-slate-700 dark:text-woot-300;
|
||||
}
|
||||
|
||||
@@ -222,20 +222,6 @@
|
||||
@apply flex relative flex-col;
|
||||
}
|
||||
|
||||
.typing-indicator-wrap {
|
||||
@apply items-center flex h-0 absolute w-full -top-8;
|
||||
|
||||
.typing-indicator {
|
||||
@include elegant-card;
|
||||
@include round-corner;
|
||||
@apply py-2 pr-4 pl-5 bg-white dark:bg-slate-700 text-slate-800 dark:text-slate-100 text-xs font-semibold my-2.5 mx-auto;
|
||||
|
||||
.gif {
|
||||
@apply ml-2 w-6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left .bubble .text-content {
|
||||
h1,
|
||||
h2,
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
// scss-lint:disable QualifyingElement
|
||||
|
||||
label {
|
||||
@apply text-slate-800 dark:text-slate-200;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply bg-white dark:bg-slate-900 focus:bg-white focus:dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
|
||||
|
||||
&[disabled] {
|
||||
@apply bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-400 border-slate-200 dark:border-slate-600;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
@apply bg-white dark:bg-slate-800;
|
||||
}
|
||||
|
||||
select {
|
||||
@apply bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600;
|
||||
}
|
||||
|
||||
.error {
|
||||
input[type='color'],
|
||||
input[type='date'],
|
||||
input[type='datetime'],
|
||||
input[type='datetime-local'],
|
||||
input[type='email'],
|
||||
input[type='month'],
|
||||
input[type='number'],
|
||||
input[type='password'],
|
||||
input[type='search'],
|
||||
input[type='tel'],
|
||||
input[type='text'],
|
||||
input[type='time'],
|
||||
input[type='url'],
|
||||
input[type='week'],
|
||||
input:not([type]),
|
||||
textarea,
|
||||
select,
|
||||
.multiselect > .multiselect__tags {
|
||||
@apply border border-solid border-red-400 dark:border-red-400;
|
||||
}
|
||||
|
||||
.message {
|
||||
@apply text-red-400 dark:text-red-400 block text-sm mb-2.5 w-full;
|
||||
}
|
||||
}
|
||||
|
||||
.button,
|
||||
textarea,
|
||||
input {
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
@apply text-slate-800 dark:text-slate-100 text-sm font-medium;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
@apply font-normal text-slate-600 dark:text-slate-400;
|
||||
}
|
||||
|
||||
.input-group.small {
|
||||
input {
|
||||
@apply text-sm h-8;
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply border-red-400 dark:border-red-400;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
.auth-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Outside login wrapper
|
||||
.login {
|
||||
@include full-height;
|
||||
overflow-y: auto;
|
||||
padding-top: $space-larger * 1.2;
|
||||
|
||||
.login__hero {
|
||||
margin-bottom: $space-larger;
|
||||
|
||||
.hero__logo {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.hero__title {
|
||||
font-weight: $font-weight-light;
|
||||
margin-top: $space-larger;
|
||||
}
|
||||
|
||||
.hero__sub {
|
||||
color: $medium-gray;
|
||||
font-size: $font-size-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// Login box
|
||||
.login-box {
|
||||
@include background-white;
|
||||
@include border-normal;
|
||||
@include elegant-card;
|
||||
|
||||
border-radius: $space-smaller;
|
||||
padding: $space-large;
|
||||
|
||||
label {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-default;
|
||||
|
||||
input {
|
||||
font-size: $font-size-default;
|
||||
height: $space-larger;
|
||||
padding: $space-slab;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
height: $space-larger;
|
||||
}
|
||||
}
|
||||
|
||||
.sigin__footer {
|
||||
font-size: $font-size-default;
|
||||
padding: $space-medium;
|
||||
|
||||
> a {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
.modal-mask {
|
||||
// @include flex;
|
||||
// @include flex-align(center, middle);
|
||||
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;
|
||||
}
|
||||
|
||||
.page-top-bar {
|
||||
@apply px-8 pt-9 pb-0;
|
||||
|
||||
img {
|
||||
@apply max-h-[3.75rem];
|
||||
}
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@apply shadow-md rounded-sm max-h-full overflow-auto relative w-[37.5rem];
|
||||
|
||||
&.medium {
|
||||
@apply max-w-[80%] w-[56.25rem];
|
||||
}
|
||||
|
||||
.content-box {
|
||||
@apply h-auto p-0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-slate-800 dark:text-slate-100 text-lg font-semibold;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-sm m-0 p-0 text-slate-600 mt-2 text-sm dark:text-slate-300;
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply p-8;
|
||||
}
|
||||
|
||||
form,
|
||||
.modal-content {
|
||||
@apply pt-4 pb-8 px-8 self-center;
|
||||
|
||||
a {
|
||||
@apply p-4;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
// @include flex;
|
||||
// @include flex-align($x: flex-end, $y: middle);
|
||||
@apply flex justify-end items-center py-2 px-0 gap-2;
|
||||
|
||||
&.justify-content-end {
|
||||
@apply justify-end;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-item {
|
||||
@apply p-8;
|
||||
|
||||
button {
|
||||
@apply m-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-enter,
|
||||
.modal-leave {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave .modal-container {
|
||||
transform: scale(1.1);
|
||||
// @apply transform scale-110;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
.reply-box {
|
||||
transition: box-shadow 0.35s $swift-ease-out-function,
|
||||
height 2s $swift-ease-out-function;
|
||||
|
||||
&.is-focused {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.reply-box__top {
|
||||
.icon {
|
||||
color: $medium-gray;
|
||||
cursor: pointer;
|
||||
font-size: $font-size-medium;
|
||||
margin-right: $space-small;
|
||||
|
||||
&.active {
|
||||
color: $color-woot;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment {
|
||||
cursor: pointer;
|
||||
margin-right: $space-one;
|
||||
padding: 0 $space-small;
|
||||
}
|
||||
|
||||
.video-js {
|
||||
background: transparent;
|
||||
// Override min-height : 50px in foundation
|
||||
//
|
||||
max-height: $space-mega * 2.4;
|
||||
min-height: 3rem;
|
||||
padding: var(--space-normal) 0 0;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
> textarea {
|
||||
@include ghost-input();
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
max-height: $space-mega * 2.4;
|
||||
// Override min-height : 50px in foundation
|
||||
min-height: 3rem;
|
||||
padding: var(--space-normal) 0 0;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-private {
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
|
||||
.reply-box__top {
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
|
||||
> input {
|
||||
@apply bg-yellow-100 dark:bg-yellow-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
.report-card {
|
||||
@include custom-border-top(3px, transparent);
|
||||
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: var(--space-normal);
|
||||
|
||||
&.active {
|
||||
@include custom-border-top(3px, var(--color-woot));
|
||||
@include background-white;
|
||||
.heading,
|
||||
.metric {
|
||||
color: var(--color-woot);
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
align-items: center;
|
||||
color: var(--color-heading);
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
color: var(--b-400);
|
||||
margin-left: var(--space-micro);
|
||||
}
|
||||
|
||||
.metric-wrap {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: var(--font-size-big);
|
||||
font-weight: var(--font-weight-feather);
|
||||
margin-top: var(--space-smaller);
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
font-size: var(--font-size-small);
|
||||
margin: 0 var(--space-small);
|
||||
}
|
||||
|
||||
.metric-up {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
.metric-down {
|
||||
color: $alert-color;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: var(--font-size-small);
|
||||
margin: 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
.reports-option__rounded--item {
|
||||
border-radius: 100%;
|
||||
height: var(--space-two);
|
||||
width: var(--space-two);
|
||||
}
|
||||
|
||||
.reports-option__item {
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.reports-option__label--swatch {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.reports-option__wrap {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.reports-option__title {
|
||||
margin: 0 var(--space-small);
|
||||
}
|
||||
|
||||
|
||||
.switch {
|
||||
margin-bottom: var(--space-zero);
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
.search {
|
||||
@include flex;
|
||||
@include flex-align($x: left, $y: middle);
|
||||
@include flex-shrink;
|
||||
|
||||
padding: $space-one $space-normal;
|
||||
transition: all 0.3s var(--ease-in-out-quad);
|
||||
|
||||
> .icon {
|
||||
color: $medium-gray;
|
||||
font-size: $font-size-medium;
|
||||
}
|
||||
|
||||
> input {
|
||||
@include ghost-input();
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
.side-menu {
|
||||
i {
|
||||
margin-right: var(--space-smaller);
|
||||
min-width: var(--space-two);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
z-index: 1024 - 1;
|
||||
|
||||
//logo
|
||||
.logo {
|
||||
img {
|
||||
max-height: 108px;
|
||||
padding: $woot-logo-padding;
|
||||
}
|
||||
}
|
||||
|
||||
.nested {
|
||||
a {
|
||||
font-size: var(--font-size-small);
|
||||
margin-bottom: var(--space-micro);
|
||||
margin-top: var(--space-micro);
|
||||
|
||||
.inbox-icon {
|
||||
display: inline-block;
|
||||
margin-right: var(--space-micro);
|
||||
min-width: var(--space-normal);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bottom-nav
|
||||
.bottom-nav {
|
||||
@include flex;
|
||||
@include space-between-column;
|
||||
@include border-normal-top;
|
||||
flex-direction: column;
|
||||
padding: var(--space-one) var(--space-normal) var(--space-one)
|
||||
var(--space-one);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-light);
|
||||
}
|
||||
|
||||
.dropdown-pane {
|
||||
bottom: 3.75rem;
|
||||
display: block;
|
||||
visibility: visible;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.active {
|
||||
border-bottom: 2px solid $medium-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger--menu {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-right: var(--space-normal);
|
||||
}
|
||||
|
||||
.header--icon {
|
||||
display: block;
|
||||
margin: 0 var(--space-small) 0 var(--space-smaller);
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0 var(--space-small);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
.ui-snackbar-container {
|
||||
left: 0;
|
||||
margin: 0 auto;
|
||||
max-width: 25rem;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
top: $space-normal;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.ui-snackbar {
|
||||
@include shadow;
|
||||
background-color: $woot-snackbar-bg;
|
||||
border-radius: $space-smaller;
|
||||
display: inline-flex;
|
||||
margin-bottom: $space-small;
|
||||
max-width: 25rem;
|
||||
min-height: 1.875rem;
|
||||
min-width: 15rem;
|
||||
padding: $space-slab $space-medium;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-snackbar-text {
|
||||
color: $color-white;
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.ui-snackbar-action {
|
||||
margin-left: auto;
|
||||
padding-left: 1.875rem;
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: 0;
|
||||
color: $woot-snackbar-button;
|
||||
font-size: $font-size-small;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// To be removed
|
||||
@@ -1,46 +0,0 @@
|
||||
.status-bar {
|
||||
@include flex;
|
||||
@include flex-align($x: center, $y: middle);
|
||||
background: lighten($warning-color, 36%);
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: $space-normal $space-smaller;
|
||||
|
||||
.message {
|
||||
font-weight: $font-weight-medium;
|
||||
margin-bottom: $zero;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: $space-smaller $zero $zero;
|
||||
padding: $space-small $space-normal;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background: lighten($alert-color, 30%);
|
||||
|
||||
.button {
|
||||
// Default and disabled states
|
||||
&,
|
||||
&.disabled,
|
||||
&[disabled],
|
||||
&.disabled:hover,
|
||||
&[disabled]:hover,
|
||||
&.disabled:focus,
|
||||
&[disabled]:focus {
|
||||
background-color: $alert-color;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: darken($alert-color, 7%);
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: lighten($warning-color, 36%);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@apply border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4;
|
||||
@apply border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4 list-none mb-0;
|
||||
}
|
||||
|
||||
.tabs--with-scroll {
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
.tabs-title {
|
||||
@apply flex-shrink-0 my-0 mx-2 ;
|
||||
@apply flex-shrink-0 my-0 mx-2;
|
||||
|
||||
.badge {
|
||||
@apply bg-slate-50 dark:bg-slate-800 rounded-md text-slate-600 dark:text-slate-100 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0;
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
|
||||
a {
|
||||
@apply flex items-center flex-row border-b border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative;
|
||||
@apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-slate-500 dark:text-slate-200 text-sm top-[1px] relative;
|
||||
transition: border-color 0.15s $swift-ease-out-function;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
table {
|
||||
@apply border-spacing-0 text-sm;
|
||||
@apply border-spacing-0 text-sm w-full;
|
||||
|
||||
thead {
|
||||
th {
|
||||
@apply font-semibold tracking-[1px] text-left uppercase text-slate-900 dark:text-slate-200;
|
||||
@apply font-semibold tracking-[1px] text-left px-2.5 uppercase text-slate-900 dark:text-slate-200;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="-mt-px text-sm">
|
||||
<button
|
||||
class="flex items-center select-none w-full bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle"
|
||||
class="flex items-center select-none w-full rounded-none bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<div class="flex justify-between mb-0.5">
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<button
|
||||
class="sm:w-[50%] md:w-[34%] lg:w-[25%] channel"
|
||||
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<img :src="src" :alt="title" />
|
||||
<h3 class="channel__title">
|
||||
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
|
||||
<h3
|
||||
class="text-slate-800 dark:text-slate-100 text-base text-center capitalize"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
</button>
|
||||
@@ -31,32 +33,8 @@ export default {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
&.channel:hover {
|
||||
&:hover {
|
||||
@apply border-transparent shadow-none cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.channel {
|
||||
@apply bg-white dark:bg-slate-900 cursor-pointer flex flex-col transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800;
|
||||
|
||||
&:hover {
|
||||
@apply border-woot-500 dark:border-woot-500 shadow-md z-50;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@apply opacity-60;
|
||||
}
|
||||
|
||||
img {
|
||||
@apply w-[50%] my-4 mx-auto;
|
||||
}
|
||||
|
||||
.channel__title {
|
||||
@apply text-slate-800 dark:text-slate-100 text-base text-center capitalize;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply text-slate-600 dark:text-slate-300 w-full text-sm;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,85 +1,23 @@
|
||||
<template>
|
||||
<div
|
||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
:class="{
|
||||
hide: !showConversationList,
|
||||
'list--full-width': isOnExpandedLayout,
|
||||
}"
|
||||
class="flex flex-col flex-shrink-0 overflow-hidden border-r conversations-list-wrap rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||
:class="[
|
||||
{ hidden: !showConversationList },
|
||||
isOnExpandedLayout ? 'basis-full' : 'flex-basis-clamp',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
<div
|
||||
class="flex items-center justify-between py-0 px-4"
|
||||
:class="{
|
||||
'pb-3 border-b border-slate-75 dark:border-slate-700':
|
||||
hasAppliedFiltersOrActiveFolders,
|
||||
}"
|
||||
>
|
||||
<div class="flex max-w-[85%] justify-center items-center">
|
||||
<h1
|
||||
class="text-xl break-words overflow-hidden whitespace-nowrap text-ellipsis text-black-900 dark:text-slate-100 mb-0"
|
||||
:title="pageTitle"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
<span
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
class="p-1 my-0.5 mx-1 rounded-md capitalize bg-slate-50 dark:bg-slate-800 text-xxs text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div v-if="hasAppliedFilters && !hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="save"
|
||||
@click="onClickOpenAddFoldersModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CLEAR_BUTTON_LABEL')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
icon="dismiss-circle"
|
||||
@click="resetAndFetchData"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.EDIT.EDIT_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="onToggleAdvanceFiltersModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
@click="onClickOpenDeleteFoldersModal"
|
||||
/>
|
||||
</div>
|
||||
<woot-button
|
||||
v-else
|
||||
v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="filter"
|
||||
size="tiny"
|
||||
@click="onToggleAdvanceFiltersModal"
|
||||
/>
|
||||
<conversation-basic-filter
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
@changeFilter="onBasicFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<chat-list-header
|
||||
:page-title="pageTitle"
|
||||
:has-applied-filters="hasAppliedFilters"
|
||||
:has-active-folders="hasActiveFolders"
|
||||
:active-status="activeStatus"
|
||||
@add-folders="onClickOpenAddFoldersModal"
|
||||
@delete-folders="onClickOpenDeleteFoldersModal"
|
||||
@filters-modal="onToggleAdvanceFiltersModal"
|
||||
@reset-filters="resetAndFetchData"
|
||||
@basic-filter-change="onBasicFilterChange"
|
||||
/>
|
||||
|
||||
<add-custom-views
|
||||
v-if="showAddFoldersModal"
|
||||
@@ -107,7 +45,7 @@
|
||||
|
||||
<p
|
||||
v-if="!chatListLoading && !conversationList.length"
|
||||
class="overflow-auto p-4 flex justify-center items-center"
|
||||
class="flex items-center justify-center p-4 overflow-auto"
|
||||
>
|
||||
{{ $t('CHAT_LIST.LIST.404') }}
|
||||
</p>
|
||||
@@ -127,7 +65,7 @@
|
||||
/>
|
||||
<div
|
||||
ref="conversationList"
|
||||
class="conversations-list flex-1"
|
||||
class="flex-1 conversations-list"
|
||||
:class="{ 'overflow-hidden': isContextMenuOpen }"
|
||||
>
|
||||
<virtual-list
|
||||
@@ -136,14 +74,17 @@
|
||||
:data-sources="conversationList"
|
||||
:data-component="itemComponent"
|
||||
:extra-props="virtualListExtraProps"
|
||||
class="w-full overflow-auto h-full"
|
||||
class="w-full h-full overflow-auto"
|
||||
footer-tag="div"
|
||||
>
|
||||
<template #footer>
|
||||
<div v-if="chatListLoading" class="text-center">
|
||||
<span class="spinner mt-4 mb-4" />
|
||||
<span class="mt-4 mb-4 spinner" />
|
||||
</div>
|
||||
<p v-if="showEndOfListMessage" class="text-center text-muted p-4">
|
||||
<p
|
||||
v-if="showEndOfListMessage"
|
||||
class="p-4 text-center text-slate-400 dark:text-slate-300"
|
||||
>
|
||||
{{ $t('CHAT_LIST.EOF') }}
|
||||
</p>
|
||||
<intersection-observer
|
||||
@@ -170,6 +111,15 @@
|
||||
@updateFolder="onUpdateSavedFilter"
|
||||
/>
|
||||
</woot-modal>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -177,12 +127,12 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import VirtualList from 'vue-virtual-scroll-list';
|
||||
|
||||
import ChatListHeader from './ChatListHeader.vue';
|
||||
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
||||
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||
import ConversationItem from './ConversationItem.vue';
|
||||
import timeMixin from '../mixins/time';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
||||
@@ -196,21 +146,21 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
hasPressedAltAndKKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { conversationListPageURL } from '../helper/URLHelper';
|
||||
import {
|
||||
isOnMentionsView,
|
||||
isOnUnattendedView,
|
||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
import IntersectionObserver from './IntersectionObserver.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatListHeader,
|
||||
AddCustomViews,
|
||||
ChatTypeTabs,
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
@@ -218,14 +168,14 @@ export default {
|
||||
ConversationAdvancedFilter,
|
||||
DeleteCustomViews,
|
||||
ConversationBulkActions,
|
||||
ConversationBasicFilter,
|
||||
IntersectionObserver,
|
||||
VirtualList,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [
|
||||
timeMixin,
|
||||
conversationMixin,
|
||||
eventListenerMixins,
|
||||
keyboardEventListenerMixins,
|
||||
alertMixin,
|
||||
filterMixin,
|
||||
uiSettingsMixin,
|
||||
@@ -290,7 +240,6 @@ export default {
|
||||
foldersQuery: {},
|
||||
showAddFoldersModal: false,
|
||||
showDeleteFoldersModal: false,
|
||||
selectedConversations: [],
|
||||
selectedInboxes: [],
|
||||
isContextMenuOpen: false,
|
||||
appliedFilter: [],
|
||||
@@ -298,6 +247,7 @@ export default {
|
||||
root: this.$refs.conversationList,
|
||||
rootMargin: '100px 0px 100px 0px',
|
||||
},
|
||||
showCustomSnoozeModal: false,
|
||||
|
||||
itemComponent: ConversationItem,
|
||||
// virtualListExtraProps is to pass the props to the conversationItem component.
|
||||
@@ -331,12 +281,14 @@ export default {
|
||||
inboxesList: 'inboxes/getInboxes',
|
||||
campaigns: 'campaigns/getAllCampaigns',
|
||||
labels: 'labels/getLabels',
|
||||
selectedConversations: 'bulkActions/getSelectedConversationIds',
|
||||
contextMenuChatId: 'getContextMenuChatId',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length !== 0;
|
||||
},
|
||||
hasActiveFolders() {
|
||||
return this.activeFolder && this.foldersId !== 0;
|
||||
return Boolean(this.activeFolder && this.foldersId !== 0);
|
||||
},
|
||||
hasAppliedFiltersOrActiveFolders() {
|
||||
return this.hasAppliedFilters || this.hasActiveFolders;
|
||||
@@ -560,6 +512,11 @@ export default {
|
||||
bus.$on('fetch_conversation_stats', () => {
|
||||
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
||||
});
|
||||
|
||||
bus.$on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
methods: {
|
||||
updateVirtualListProps(key, value) {
|
||||
@@ -688,30 +645,40 @@ export default {
|
||||
lastConversationIndex,
|
||||
};
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndJKey(e)) {
|
||||
const { allConversations, activeConversationIndex } =
|
||||
this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
if (activeConversationIndex >= 1) {
|
||||
allConversations[activeConversationIndex - 1].click();
|
||||
}
|
||||
handlePreviousConversation() {
|
||||
const { allConversations, activeConversationIndex } =
|
||||
this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[0].click();
|
||||
}
|
||||
if (hasPressedAltAndKKey(e)) {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
}
|
||||
if (activeConversationIndex >= 1) {
|
||||
allConversations[activeConversationIndex - 1].click();
|
||||
}
|
||||
},
|
||||
handleNextConversation() {
|
||||
const {
|
||||
allConversations,
|
||||
activeConversationIndex,
|
||||
lastConversationIndex,
|
||||
} = this.getKeyboardListenerParams();
|
||||
if (activeConversationIndex === -1) {
|
||||
allConversations[lastConversationIndex].click();
|
||||
} else if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
}
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyJ': {
|
||||
action: () => this.handlePreviousConversation(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyK': {
|
||||
action: () => this.handleNextConversation(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
resetAndFetchData() {
|
||||
this.appliedFilter = [];
|
||||
this.resetBulkActions();
|
||||
@@ -791,7 +758,7 @@ export default {
|
||||
});
|
||||
},
|
||||
resetBulkActions() {
|
||||
this.selectedConversations = [];
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
this.selectedInboxes = [];
|
||||
},
|
||||
onBasicFilterChange(value, type) {
|
||||
@@ -822,12 +789,16 @@ export default {
|
||||
return this.selectedConversations.includes(id);
|
||||
},
|
||||
selectConversation(conversationId, inboxId) {
|
||||
this.selectedConversations.push(conversationId);
|
||||
this.$store.dispatch(
|
||||
'bulkActions/setSelectedConversationIds',
|
||||
conversationId
|
||||
);
|
||||
this.selectedInboxes.push(inboxId);
|
||||
},
|
||||
deSelectConversation(conversationId, inboxId) {
|
||||
this.selectedConversations = this.selectedConversations.filter(
|
||||
item => item !== conversationId
|
||||
this.$store.dispatch(
|
||||
'bulkActions/removeSelectedConversationIds',
|
||||
conversationId
|
||||
);
|
||||
this.selectedInboxes = this.selectedInboxes.filter(
|
||||
item => item !== inboxId
|
||||
@@ -835,7 +806,10 @@ export default {
|
||||
},
|
||||
selectAllConversations(check) {
|
||||
if (check) {
|
||||
this.selectedConversations = this.conversationList.map(item => item.id);
|
||||
this.$store.dispatch(
|
||||
'bulkActions/setSelectedConversationIds',
|
||||
this.conversationList.map(item => item.id)
|
||||
);
|
||||
this.selectedInboxes = this.conversationList.map(item => item.inbox_id);
|
||||
} else {
|
||||
this.resetBulkActions();
|
||||
@@ -851,7 +825,7 @@ export default {
|
||||
assignee_id: agent.id,
|
||||
},
|
||||
});
|
||||
this.selectedConversations = [];
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
if (conversationId) {
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
@@ -949,7 +923,7 @@ export default {
|
||||
add: labels,
|
||||
},
|
||||
});
|
||||
this.selectedConversations = [];
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
if (conversationId) {
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
@@ -976,13 +950,13 @@ export default {
|
||||
team_id: team.id,
|
||||
},
|
||||
});
|
||||
this.selectedConversations = [];
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
|
||||
} catch (err) {
|
||||
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
|
||||
}
|
||||
},
|
||||
async onUpdateConversations(status) {
|
||||
async onUpdateConversations(status, snoozedUntil) {
|
||||
try {
|
||||
await this.$store.dispatch('bulkActions/process', {
|
||||
type: 'Conversation',
|
||||
@@ -990,8 +964,9 @@ export default {
|
||||
fields: {
|
||||
status,
|
||||
},
|
||||
snoozed_until: snoozedUntil,
|
||||
});
|
||||
this.selectedConversations = [];
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
|
||||
} catch (err) {
|
||||
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
|
||||
@@ -1018,6 +993,43 @@ export default {
|
||||
onContextMenuToggle(state) {
|
||||
this.isContextMenuOpen = state;
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.showCustomSnoozeModal = true;
|
||||
} else {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
findSnoozeTime(snoozeType) || null
|
||||
);
|
||||
}
|
||||
},
|
||||
chooseSnoozeTime(customSnoozeTime) {
|
||||
this.showCustomSnoozeModal = false;
|
||||
if (customSnoozeTime) {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
getUnixTime(customSnoozeTime)
|
||||
);
|
||||
}
|
||||
},
|
||||
toggleStatus(status, snoozedUntil) {
|
||||
this.$store
|
||||
.dispatch('toggleStatus', {
|
||||
conversationId: this.currentChat?.id || this.contextMenuChatId,
|
||||
status,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
});
|
||||
},
|
||||
hideCustomSnoozeModal() {
|
||||
// if we select custom snooze and then the custom snooze modal is open
|
||||
// Then if the custom snooze modal is closed and set the context menu chat id to null
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showCustomSnoozeModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1031,24 +1043,10 @@ export default {
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversations-list-wrap {
|
||||
&.hide {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
&.list--full-width {
|
||||
@apply basis-full;
|
||||
}
|
||||
}
|
||||
|
||||
.conversations-list {
|
||||
@apply overflow-hidden hover:overflow-y-auto;
|
||||
}
|
||||
|
||||
.load-more--button {
|
||||
@apply text-center rounded-none;
|
||||
}
|
||||
|
||||
.tab--chat-type {
|
||||
@apply py-0 px-4;
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
|
||||
|
||||
const props = defineProps({
|
||||
pageTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasAppliedFilters: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
hasActiveFolders: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
activeStatus: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits([
|
||||
'add-folders',
|
||||
'delete-folders',
|
||||
'reset-filters',
|
||||
'basic-filter-change',
|
||||
'filters-modal',
|
||||
]);
|
||||
|
||||
const onBasicFilterChange = (value, type) => {
|
||||
emits('basic-filter-change', value, type);
|
||||
};
|
||||
|
||||
const hasAppliedFiltersOrActiveFolders = computed(() => {
|
||||
return props.hasAppliedFilters || props.hasActiveFolders;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-0"
|
||||
:class="{
|
||||
'pb-3 border-b border-slate-75 dark:border-slate-700':
|
||||
hasAppliedFiltersOrActiveFolders,
|
||||
}"
|
||||
>
|
||||
<div class="flex max-w-[85%] justify-center items-center">
|
||||
<h1
|
||||
class="text-xl font-medium break-words truncate text-black-900 dark:text-slate-100"
|
||||
:title="pageTitle"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
</h1>
|
||||
<span
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
class="p-1 my-0.5 mx-1 rounded-md capitalize bg-slate-50 dark:bg-slate-800 text-xxs text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ $t(`CHAT_LIST.CHAT_STATUS_FILTER_ITEMS.${activeStatus}.TEXT`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div v-if="hasAppliedFilters && !hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="save"
|
||||
@click="emits('add-folders')"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CLEAR_BUTTON_LABEL')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
icon="dismiss-circle"
|
||||
@click="emits('reset-filters')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.EDIT.EDIT_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="emits('filters-modal')"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
@click="emits('delete-folders')"
|
||||
/>
|
||||
</div>
|
||||
<woot-button
|
||||
v-else
|
||||
v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="filter"
|
||||
size="tiny"
|
||||
@click="emits('filters-modal')"
|
||||
/>
|
||||
<conversation-basic-filter
|
||||
v-if="!hasAppliedFiltersOrActiveFolders"
|
||||
@changeFilter="onBasicFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,24 +1,33 @@
|
||||
<template>
|
||||
<div class="py-3 px-4">
|
||||
<div class="flex items-center mb-1">
|
||||
<h4 class="text-block-title flex items-center m-0 w-full error">
|
||||
<div v-if="isAttributeTypeCheckbox" class="checkbox-wrap">
|
||||
<h4 class="text-sm flex items-center m-0 w-full error">
|
||||
<div v-if="isAttributeTypeCheckbox" class="flex items-center">
|
||||
<input
|
||||
v-model="editedValue"
|
||||
class="checkbox"
|
||||
class="!my-0 mr-2 ml-0"
|
||||
type="checkbox"
|
||||
@change="onUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span
|
||||
class="attribute-name"
|
||||
:class="{ error: $v.editedValue.$error }"
|
||||
class="w-full inline-flex gap-1.5 items-start font-medium whitespace-nowrap text-sm mb-0"
|
||||
:class="
|
||||
$v.editedValue.$error
|
||||
? 'text-red-400 dark:text-red-500'
|
||||
: 'text-slate-800 dark:text-slate-100'
|
||||
"
|
||||
>
|
||||
{{ label }}
|
||||
<helper-text-popup
|
||||
v-if="description"
|
||||
:message="description"
|
||||
class="mt-0.5"
|
||||
/>
|
||||
</span>
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-if="showCopyAndDeleteButton"
|
||||
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
|
||||
variant="link"
|
||||
size="medium"
|
||||
@@ -31,20 +40,25 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div v-if="notAttributeTypeCheckboxAndList">
|
||||
<div v-show="isEditing">
|
||||
<div class="input-group small">
|
||||
<div v-if="isEditing" v-on-clickaway="onClickAway">
|
||||
<div class="mb-2 w-full flex items-center">
|
||||
<input
|
||||
ref="inputfield"
|
||||
v-model="editedValue"
|
||||
:type="inputType"
|
||||
class="input-group-field"
|
||||
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"
|
||||
@keyup.enter="onUpdate"
|
||||
/>
|
||||
<div class="input-group-button">
|
||||
<woot-button size="small" icon="checkmark" @click="onUpdate" />
|
||||
<div>
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="checkmark"
|
||||
class="rounded-l-none rtl:rounded-r-none"
|
||||
@click="onUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -56,7 +70,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-show="!isEditing"
|
||||
class="value--view"
|
||||
class="flex group"
|
||||
:class="{ 'is-editable': showActions }"
|
||||
>
|
||||
<a
|
||||
@@ -64,35 +78,35 @@
|
||||
:href="hrefURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1"
|
||||
class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
|
||||
>
|
||||
{{ urlValue }}
|
||||
</a>
|
||||
<p
|
||||
v-else
|
||||
class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1"
|
||||
class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
|
||||
>
|
||||
{{ displayValue || '---' }}
|
||||
</p>
|
||||
<div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0">
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-if="showCopyAndDeleteButton"
|
||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="clipboard"
|
||||
class-names="edit-button"
|
||||
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
|
||||
@click="onCopy"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-if="showEditButton"
|
||||
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
class-names="edit-button"
|
||||
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
|
||||
@click="onEdit"
|
||||
/>
|
||||
</div>
|
||||
@@ -125,17 +139,21 @@ 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],
|
||||
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 },
|
||||
@@ -155,11 +173,18 @@ export default {
|
||||
editedValue: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
showCopyAndDeleteButton() {
|
||||
return this.value && this.showActions;
|
||||
},
|
||||
showEditButton() {
|
||||
return !this.value && this.showActions;
|
||||
},
|
||||
displayValue() {
|
||||
if (this.isAttributeTypeDate) {
|
||||
return new Date(this.value || new Date()).toLocaleDateString();
|
||||
return this.value
|
||||
? new Date(this.value || new Date()).toLocaleDateString()
|
||||
: '';
|
||||
}
|
||||
if (this.isAttributeTypeCheckbox) {
|
||||
return this.value === 'false' ? false : this.value;
|
||||
@@ -225,6 +250,10 @@ export default {
|
||||
this.isEditing = false;
|
||||
this.editedValue = this.formattedValue;
|
||||
},
|
||||
contactId() {
|
||||
// Fix to solve validation not resetting when contactId changes in contact page
|
||||
this.$v.$reset();
|
||||
},
|
||||
},
|
||||
|
||||
validations() {
|
||||
@@ -263,6 +292,10 @@ export default {
|
||||
this.$refs.inputfield.focus();
|
||||
}
|
||||
},
|
||||
onClickAway() {
|
||||
this.$v.$reset();
|
||||
this.isEditing = false;
|
||||
},
|
||||
onEdit() {
|
||||
this.isEditing = true;
|
||||
this.$nextTick(() => {
|
||||
@@ -289,6 +322,7 @@ export default {
|
||||
},
|
||||
onDelete() {
|
||||
this.isEditing = false;
|
||||
this.$v.$reset();
|
||||
this.$emit('delete', this.attributeKey);
|
||||
},
|
||||
onCopy() {
|
||||
@@ -299,36 +333,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.checkbox-wrap {
|
||||
@apply flex items-center;
|
||||
}
|
||||
.checkbox {
|
||||
@apply my-0 mr-2 ml-0;
|
||||
}
|
||||
.attribute-name {
|
||||
@apply w-full text-slate-800 dark:text-slate-100;
|
||||
&.error {
|
||||
@apply text-red-400 dark:text-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.value--view {
|
||||
@apply flex;
|
||||
|
||||
&.is-editable:hover {
|
||||
.value {
|
||||
@apply bg-slate-50 dark:bg-slate-700 mb-0;
|
||||
}
|
||||
.edit-button {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.selector-wrap {
|
||||
@apply m-0 top-1;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<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>
|
||||
@@ -7,7 +7,13 @@
|
||||
@mousedown="handleMouseDown"
|
||||
>
|
||||
<div
|
||||
:class="modalContainerClassName"
|
||||
:class="{
|
||||
'modal-container rtl:text-right shadow-md max-h-full overflow-auto relative bg-white dark:bg-slate-800 skip-context-menu': true,
|
||||
'rounded-xl w-[37.5rem]': !fullWidth,
|
||||
'items-center rounded-none flex h-full justify-center w-full':
|
||||
fullWidth,
|
||||
[size]: true,
|
||||
}"
|
||||
@mouse.stop
|
||||
@mousedown="event => event.stopPropagation()"
|
||||
>
|
||||
@@ -16,7 +22,7 @@
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
class="absolute ltr:right-2 rtl:left-2 top-2 z-10"
|
||||
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
|
||||
@click="close"
|
||||
/>
|
||||
<slot />
|
||||
@@ -60,15 +66,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modalContainerClassName() {
|
||||
let className =
|
||||
'modal-container bg-white dark:bg-slate-800 skip-context-menu';
|
||||
if (this.fullWidth) {
|
||||
return `${className} modal-container--full-width`;
|
||||
}
|
||||
|
||||
return `${className} ${this.size}`;
|
||||
},
|
||||
modalClassName() {
|
||||
const modalClassNameMap = {
|
||||
centered: '',
|
||||
@@ -109,26 +106,43 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-container--full-width {
|
||||
align-items: center;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-mask.right-aligned {
|
||||
justify-content: flex-end;
|
||||
|
||||
<style lang="scss">
|
||||
.modal-mask {
|
||||
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;
|
||||
.modal-container {
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
width: 30rem;
|
||||
&.medium {
|
||||
@apply max-w-[80%] w-[56.25rem];
|
||||
}
|
||||
// .content-box {
|
||||
// @apply h-auto p-0;
|
||||
// }
|
||||
.content {
|
||||
@apply p-8;
|
||||
}
|
||||
form,
|
||||
.modal-content {
|
||||
@apply pt-4 pb-8 px-8 self-center;
|
||||
a {
|
||||
@apply p-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-big {
|
||||
width: 60%;
|
||||
@apply w-full;
|
||||
}
|
||||
.modal-mask.right-aligned {
|
||||
@apply justify-end;
|
||||
.modal-container {
|
||||
@apply rounded-none h-full w-[30rem];
|
||||
}
|
||||
}
|
||||
.modal-enter,
|
||||
.modal-leave {
|
||||
@apply opacity-0;
|
||||
}
|
||||
.modal-enter .modal-container,
|
||||
.modal-leave .modal-container {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-start pt-8 px-8 pb-0">
|
||||
<div class="flex flex-col items-start px-8 pt-8 pb-0">
|
||||
<img v-if="headerImage" :src="headerImage" alt="No image" />
|
||||
<h2
|
||||
ref="modalHeaderTitle"
|
||||
class="text-slate-800 text-lg dark:text-slate-100"
|
||||
class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="headerContent"
|
||||
ref="modalHeaderContent"
|
||||
class="w-full break-words text-slate-600 mt-2 text-sm dark:text-slate-300"
|
||||
class="w-full mt-2 text-sm leading-5 break-words text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ headerContent }}
|
||||
<span
|
||||
v-if="headerContentValue"
|
||||
class="font-semibold text-sm text-slate-600 dark:text-slate-300"
|
||||
class="text-sm font-semibold text-slate-600 dark:text-slate-300"
|
||||
>
|
||||
{{ headerContentValue }}
|
||||
</span>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<template>
|
||||
<div
|
||||
class="ml-0 mr-0 flex pt-0 pr-4 pb-4 pl-0"
|
||||
class="ml-0 mr-0 flex py-8 w-full xl:w-3/4 flex-col xl:flex-row"
|
||||
:class="{
|
||||
'pt-4 border-b border-solid border-slate-50 dark:border-slate-700/30':
|
||||
'border-b border-solid border-slate-50 dark:border-slate-700/30':
|
||||
showBorder,
|
||||
}"
|
||||
>
|
||||
<div class="w-[30%] min-w-0 max-w-[30%] pr-12">
|
||||
<div class="w-full xl:w-1/4 min-w-0 xl:max-w-[30%] pr-12">
|
||||
<p
|
||||
v-if="title"
|
||||
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
|
||||
>
|
||||
{{ title }}
|
||||
</p>
|
||||
<p class="text-sm mb-2">
|
||||
<p
|
||||
class="text-sm mb-2 text-slate-700 dark:text-slate-300 leading-5 tracking-normal mt-2"
|
||||
>
|
||||
<slot v-if="subTitle" name="subTitle">
|
||||
{{ subTitle }}
|
||||
</slot>
|
||||
@@ -23,7 +25,7 @@
|
||||
{{ note }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-[50%] min-w-0 max-w-[50%]">
|
||||
<div class="w-full xl:w-1/2 min-w-0 xl:max-w-[50%]">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="ui-snackbar">
|
||||
<div class="ui-snackbar-text">
|
||||
<div
|
||||
class="shadow-sm bg-slate-800 dark:bg-slate-700 rounded-[4px] items-center gap-3 inline-flex mb-2 max-w-[25rem] min-h-[1.875rem] min-w-[15rem] px-6 py-3 text-left"
|
||||
>
|
||||
<div class="text-white dark:text-white text-sm font-medium">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div v-if="action" class="ui-snackbar-action">
|
||||
<router-link v-if="action.type == 'link'" :to="action.to">
|
||||
<div v-if="action">
|
||||
<router-link
|
||||
v-if="action.type == 'link'"
|
||||
:to="action.to"
|
||||
class="text-woot-500 dark:text-woot-500 cursor-pointer font-medium hover:text-woot-600 dark:hover:text-woot-600 select-none"
|
||||
>
|
||||
{{ action.message }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<transition-group name="toast-fade" tag="div" class="ui-snackbar-container">
|
||||
<transition-group
|
||||
name="toast-fade"
|
||||
tag="div"
|
||||
class="left-0 my-0 mx-auto max-w-[25rem] overflow-hidden absolute right-0 text-center top-4 z-[9999]"
|
||||
>
|
||||
<woot-snackbar
|
||||
v-for="snackMessage in snackMessages"
|
||||
:key="snackMessage.key"
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-menu class="mb-0">
|
||||
<woot-dropdown-item v-if="!isPending">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
@@ -73,31 +73,13 @@
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import {
|
||||
hasPressedAltAndEKey,
|
||||
hasPressedCommandPlusAltAndEKey,
|
||||
hasPressedAltAndMKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
|
||||
@@ -105,23 +87,20 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
import {
|
||||
CMD_REOPEN_CONVERSATION,
|
||||
CMD_RESOLVE_CONVERSATION,
|
||||
CMD_SNOOZE_CONVERSATION,
|
||||
} from '../../routes/dashboard/commands/commandBarBusEvents';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||
mixins: [alertMixin, keyboardEventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
showActionsDropdown: false,
|
||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||
showCustomSnoozeModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -149,71 +128,62 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
bus.$on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
bus.$on(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
|
||||
bus.$on(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
},
|
||||
destroyed() {
|
||||
bus.$off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
bus.$off(CMD_REOPEN_CONVERSATION, this.onCmdOpenConversation);
|
||||
bus.$off(CMD_RESOLVE_CONVERSATION, this.onCmdResolveConversation);
|
||||
},
|
||||
methods: {
|
||||
async handleKeyEvents(e) {
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyM': {
|
||||
action: () => this.$refs.arrowDownButton?.$el.click(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
'Alt+KeyE': this.resolveOrToast,
|
||||
'$mod+Alt+KeyE': async event => {
|
||||
const { all, activeIndex, lastIndex } = this.getConversationParams();
|
||||
await this.resolveOrToast();
|
||||
|
||||
if (activeIndex < lastIndex) {
|
||||
all[activeIndex + 1].click();
|
||||
} else if (all.length > 1) {
|
||||
all[0].click();
|
||||
document.querySelector('.conversations-list').scrollTop = 0;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
};
|
||||
},
|
||||
getConversationParams() {
|
||||
const allConversations = document.querySelectorAll(
|
||||
'.conversations-list .conversation'
|
||||
);
|
||||
if (hasPressedAltAndMKey(e)) {
|
||||
if (this.$refs.arrowDownButton) {
|
||||
this.$refs.arrowDownButton.$el.click();
|
||||
}
|
||||
}
|
||||
if (hasPressedAltAndEKey(e)) {
|
||||
const activeConversation = document.querySelector(
|
||||
'div.conversations-list div.conversation.active'
|
||||
);
|
||||
const activeConversationIndex = [...allConversations].indexOf(
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
try {
|
||||
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
if (hasPressedCommandPlusAltAndEKey(e)) {
|
||||
if (activeConversationIndex < lastConversationIndex) {
|
||||
allConversations[activeConversationIndex + 1].click();
|
||||
} else if (allConversations.length > 1) {
|
||||
allConversations[0].click();
|
||||
document.querySelector('.conversations-list').scrollTop = 0;
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
const activeConversation = document.querySelector(
|
||||
'div.conversations-list div.conversation.active'
|
||||
);
|
||||
const activeConversationIndex = [...allConversations].indexOf(
|
||||
activeConversation
|
||||
);
|
||||
const lastConversationIndex = allConversations.length - 1;
|
||||
|
||||
return {
|
||||
all: allConversations,
|
||||
activeIndex: activeConversationIndex,
|
||||
lastIndex: lastConversationIndex,
|
||||
};
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.showCustomSnoozeModal = true;
|
||||
} else {
|
||||
this.toggleStatus(
|
||||
this.STATUS_TYPE.SNOOZED,
|
||||
findSnoozeTime(snoozeType) || null
|
||||
);
|
||||
async resolveOrToast() {
|
||||
try {
|
||||
await this.toggleStatus(wootConstants.STATUS_TYPE.RESOLVED);
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
},
|
||||
chooseSnoozeTime(customSnoozeTime) {
|
||||
this.showCustomSnoozeModal = false;
|
||||
if (customSnoozeTime) {
|
||||
this.toggleStatus(
|
||||
this.STATUS_TYPE.SNOOZED,
|
||||
getUnixTime(customSnoozeTime)
|
||||
);
|
||||
}
|
||||
},
|
||||
hideCustomSnoozeModal() {
|
||||
this.showCustomSnoozeModal = false;
|
||||
},
|
||||
onCmdOpenConversation() {
|
||||
this.toggleStatus(this.STATUS_TYPE.OPEN);
|
||||
},
|
||||
|
||||
@@ -18,13 +18,13 @@ import Label from './ui/Label';
|
||||
import LoadingState from './widgets/LoadingState';
|
||||
import Modal from './Modal';
|
||||
import ModalHeader from './ModalHeader';
|
||||
import ReportStatsCard from './widgets/ReportStatsCard';
|
||||
import SidemenuIcon from './SidemenuIcon';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
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,
|
||||
@@ -46,13 +46,13 @@ const WootUIKit = {
|
||||
LoadingState,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ReportStatsCard,
|
||||
SidemenuIcon,
|
||||
Spinner,
|
||||
SubmitButton,
|
||||
Tabs,
|
||||
TabsItem,
|
||||
Thumbnail,
|
||||
DatePicker,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
@@ -67,7 +66,7 @@ export default {
|
||||
AvailabilityStatusBadge,
|
||||
},
|
||||
|
||||
mixins: [clickaway, alertMixin],
|
||||
mixins: [alertMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="h-full flex">
|
||||
<aside class="flex h-full">
|
||||
<primary-sidebar
|
||||
:logo-source="globalConfig.logoThumbnail"
|
||||
:installation-name="globalConfig.installationName"
|
||||
@@ -36,15 +36,7 @@ import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||
import {
|
||||
hasPressedAltAndCKey,
|
||||
hasPressedAltAndRKey,
|
||||
hasPressedAltAndSKey,
|
||||
hasPressedAltAndVKey,
|
||||
hasPressedCommandAndForwardSlash,
|
||||
isEscape,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import router from '../../routes';
|
||||
|
||||
export default {
|
||||
@@ -52,7 +44,7 @@ export default {
|
||||
PrimarySidebar,
|
||||
SecondarySidebar,
|
||||
},
|
||||
mixins: [adminMixin, alertMixin, eventListenerMixins],
|
||||
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
showSecondarySidebar: {
|
||||
type: Boolean,
|
||||
@@ -173,30 +165,27 @@ export default {
|
||||
closeKeyShortcutModal() {
|
||||
this.$emit('close-key-shortcut-modal');
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedCommandAndForwardSlash(e)) {
|
||||
this.toggleKeyShortcutModal();
|
||||
}
|
||||
if (isEscape(e)) {
|
||||
this.closeKeyShortcutModal();
|
||||
}
|
||||
|
||||
if (hasPressedAltAndCKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('home')) {
|
||||
router.push({ name: 'home' });
|
||||
}
|
||||
} else if (hasPressedAltAndVKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('contacts_dashboard')) {
|
||||
router.push({ name: 'contacts_dashboard' });
|
||||
}
|
||||
} else if (hasPressedAltAndRKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('settings_account_reports')) {
|
||||
router.push({ name: 'settings_account_reports' });
|
||||
}
|
||||
} else if (hasPressedAltAndSKey(e)) {
|
||||
if (!this.isCurrentRouteSameAsNavigation('agent_list')) {
|
||||
router.push({ name: 'agent_list' });
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+Slash': this.toggleKeyShortcutModal,
|
||||
'$mod+Escape': this.closeKeyShortcutModal,
|
||||
'Alt+KeyC': {
|
||||
action: () => this.navigateToRoute('home'),
|
||||
},
|
||||
'Alt+KeyV': {
|
||||
action: () => this.navigateToRoute('contacts_dashboard'),
|
||||
},
|
||||
'Alt+KeyR': {
|
||||
action: () => this.navigateToRoute('account_overview_reports'),
|
||||
},
|
||||
'Alt+KeyS': {
|
||||
action: () => this.navigateToRoute('agent_list'),
|
||||
},
|
||||
};
|
||||
},
|
||||
navigateToRoute(routeName) {
|
||||
if (!this.isCurrentRouteSameAsNavigation(routeName)) {
|
||||
router.push({ name: routeName });
|
||||
}
|
||||
},
|
||||
isCurrentRouteSameAsNavigation(routeName) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const campaigns = accountId => ({
|
||||
parentNav: 'campaigns',
|
||||
routes: ['settings_account_campaigns', 'one_off'],
|
||||
routes: ['ongoing_campaigns', 'one_off'],
|
||||
menuItems: [
|
||||
{
|
||||
icon: 'arrow-swap',
|
||||
@@ -10,7 +10,7 @@ const campaigns = accountId => ({
|
||||
key: 'ongoingCampaigns',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
toStateName: 'ongoing_campaigns',
|
||||
},
|
||||
{
|
||||
key: 'oneOffCampaigns',
|
||||
|
||||
@@ -2,6 +2,15 @@ import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const primaryMenuItems = accountId => [
|
||||
{
|
||||
icon: 'mail-inbox',
|
||||
key: 'inboxView',
|
||||
label: 'INBOX_VIEW',
|
||||
featureFlag: FEATURE_FLAGS.INBOX_VIEW,
|
||||
toState: frontendURL(`accounts/${accountId}/inbox-view`),
|
||||
toStateName: 'inbox_view',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
{
|
||||
icon: 'chat',
|
||||
key: 'conversations',
|
||||
@@ -34,7 +43,7 @@ const primaryMenuItems = accountId => [
|
||||
label: 'CAMPAIGNS',
|
||||
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
toStateName: 'ongoing_campaigns',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
const reports = accountId => ({
|
||||
@@ -6,10 +7,12 @@ const reports = accountId => ({
|
||||
'account_overview_reports',
|
||||
'conversation_reports',
|
||||
'csat_reports',
|
||||
'bot_reports',
|
||||
'agent_reports',
|
||||
'label_reports',
|
||||
'inbox_reports',
|
||||
'team_reports',
|
||||
'sla_reports',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
@@ -33,6 +36,14 @@ const reports = accountId => ({
|
||||
toState: frontendURL(`accounts/${accountId}/reports/csat`),
|
||||
toStateName: 'csat_reports',
|
||||
},
|
||||
{
|
||||
icon: 'bot',
|
||||
label: 'REPORTS_BOT',
|
||||
hasSubMenu: false,
|
||||
featureFlag: FEATURE_FLAGS.RESPONSE_BOT,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/bot`),
|
||||
toStateName: 'bot_reports',
|
||||
},
|
||||
{
|
||||
icon: 'people',
|
||||
label: 'REPORTS_AGENT',
|
||||
@@ -61,6 +72,14 @@ const reports = accountId => ({
|
||||
toState: frontendURL(`accounts/${accountId}/reports/teams`),
|
||||
toStateName: 'team_reports',
|
||||
},
|
||||
{
|
||||
icon: 'document-list-clock',
|
||||
label: 'REPORTS_SLA',
|
||||
hasSubMenu: false,
|
||||
featureFlag: FEATURE_FLAGS.SLA,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/sla`),
|
||||
toStateName: 'sla_reports',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const settings = accountId => ({
|
||||
'settings_teams_finish',
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'sla_list',
|
||||
],
|
||||
menuItems: [
|
||||
{
|
||||
@@ -141,6 +142,26 @@ const settings = accountId => ({
|
||||
toStateName: 'settings_applications',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
{
|
||||
icon: 'key',
|
||||
label: 'AUDIT_LOGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
|
||||
toStateName: 'auditlogs_list',
|
||||
isEnterpriseOnly: true,
|
||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
icon: 'document-list-clock',
|
||||
label: 'SLA',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
|
||||
toStateName: 'sla_list',
|
||||
isEnterpriseOnly: true,
|
||||
featureFlag: FEATURE_FLAGS.SLA,
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
icon: 'credit-card-person',
|
||||
label: 'BILLING',
|
||||
@@ -149,15 +170,6 @@ const settings = accountId => ({
|
||||
toStateName: 'billing_settings_index',
|
||||
showOnlyOnCloud: true,
|
||||
},
|
||||
{
|
||||
icon: 'key',
|
||||
label: 'AUDIT_LOGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
|
||||
toStateName: 'auditlogs_list',
|
||||
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
|
||||
beta: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
||||
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
<div class="px-8 pt-4 pb-8">
|
||||
<div class="px-8 py-4">
|
||||
<div
|
||||
v-for="account in currentUser.accounts"
|
||||
:id="`account-${account.id}`"
|
||||
@@ -45,10 +45,10 @@
|
||||
|
||||
<div
|
||||
v-if="globalConfig.createNewAccountFromDashboard"
|
||||
class="modal-footer delete-item"
|
||||
class="flex justify-end items-center px-8 pb-8 pt-4 gap-2"
|
||||
>
|
||||
<button
|
||||
class="button success large expanded nice"
|
||||
class="button success large expanded nice w-full"
|
||||
@click="$emit('show-create-account-modal')"
|
||||
>
|
||||
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<woot-button
|
||||
v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)"
|
||||
variant="link"
|
||||
class="current-user"
|
||||
class="items-center flex rounded-full"
|
||||
@click="handleClick"
|
||||
>
|
||||
<thumbnail
|
||||
@@ -38,12 +38,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.current-user {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--white);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="mb-4">
|
||||
<button
|
||||
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
|
||||
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 p-0 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
|
||||
:class="{
|
||||
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
|
||||
isNotificationPanelActive,
|
||||
@@ -16,7 +16,7 @@
|
||||
/>
|
||||
<span
|
||||
v-if="unreadCount"
|
||||
class="text-black-900 bg-yellow-300 absolute -top-0.5 -right-1 p-1 text-xxs min-w-[1rem] rounded-full"
|
||||
class="text-black-900 bg-yellow-300 absolute -top-0.5 -right-1 text-xxs min-w-[1rem] rounded-full"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</span>
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Auth from '../../../api/auth';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
@@ -119,7 +118,6 @@ export default {
|
||||
WootDropdownItem,
|
||||
AvailabilityStatus,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@
|
||||
</span>
|
||||
<span
|
||||
v-if="showChildCount"
|
||||
class="bg-slate-50 dark:bg-slate-700 rounded text-xxs font-medium mx-1 py-0 px-1"
|
||||
class="bg-slate-50 dark:bg-slate-700 rounded-full min-w-[18px] justify-center items-center flex text-xxs font-medium mx-1 py-0 px-1"
|
||||
:class="
|
||||
isCountZero
|
||||
? `text-slate-300 dark:text-slate-500`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<li v-show="isMenuItemVisible" class="mt-1">
|
||||
<div v-if="hasSubMenu" class="flex justify-between">
|
||||
<span
|
||||
class="text-sm text-slate-700 dark:text-slate-200 font-semibold my-2 px-2 pt-1"
|
||||
class="px-2 pt-1 my-2 text-sm font-semibold text-slate-700 dark:text-slate-200"
|
||||
>
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
</span>
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
<router-link
|
||||
v-else
|
||||
class="rounded-lg leading-4 font-medium flex items-center p-2 m-0 text-sm text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
|
||||
class="flex items-center p-2 m-0 text-sm font-medium leading-4 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
|
||||
:class="computedClass"
|
||||
:to="menuItem && menuItem.toState"
|
||||
>
|
||||
@@ -31,7 +31,7 @@
|
||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||
<span
|
||||
v-if="showChildCount(menuItem.count)"
|
||||
class="rounded-md text-xxs font-medium mx-1 py-0 px-1"
|
||||
class="px-1 py-0 mx-1 font-medium rounded-md text-xxs"
|
||||
:class="{
|
||||
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
|
||||
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
|
||||
@@ -46,13 +46,13 @@
|
||||
v-if="menuItem.beta"
|
||||
data-view-component="true"
|
||||
label="Beta"
|
||||
class="px-1 mx-1 inline-block font-medium leading-4 border border-green-400 text-green-500 rounded-lg text-xxs"
|
||||
class="inline-block px-1 mx-1 font-medium leading-4 text-green-500 border border-green-400 rounded-lg text-xxs"
|
||||
>
|
||||
{{ $t('SIDEBAR.BETA') }}
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<ul v-if="hasSubMenu" class="list-none ml-0 mb-0">
|
||||
<ul v-if="hasSubMenu" class="mb-0 ml-0 list-none">
|
||||
<secondary-child-nav-item
|
||||
v-for="child in menuItem.children"
|
||||
:key="child.id"
|
||||
@@ -94,6 +94,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import adminMixin from '../../../mixins/isAdmin';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import {
|
||||
getInboxClassByType,
|
||||
getInboxWarningIconClass,
|
||||
@@ -107,7 +108,7 @@ import {
|
||||
|
||||
export default {
|
||||
components: { SecondaryChildNavItem },
|
||||
mixins: [adminMixin],
|
||||
mixins: [adminMixin, configMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
@@ -132,15 +133,33 @@ export default {
|
||||
},
|
||||
isMenuItemVisible() {
|
||||
if (this.menuItem.globalConfigFlag) {
|
||||
// this checks for the `csmlEditorHost` flag in the global config
|
||||
// if this is present, we toggle the CSML editor menu item
|
||||
// TODO: This is very specific, and can be handled better, fix it
|
||||
return !!this.globalConfig[this.menuItem.globalConfigFlag];
|
||||
}
|
||||
|
||||
let isFeatureEnabled = true;
|
||||
if (this.menuItem.featureFlag) {
|
||||
isFeatureEnabled = this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
this.menuItem.featureFlag
|
||||
);
|
||||
}
|
||||
|
||||
if (this.menuItem.isEnterpriseOnly) {
|
||||
if (!this.isEnterprise) return false;
|
||||
return isFeatureEnabled || this.globalConfig.displayManifest;
|
||||
}
|
||||
|
||||
if (this.menuItem.featureFlag) {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
this.menuItem.featureFlag
|
||||
);
|
||||
}
|
||||
return true;
|
||||
|
||||
return isFeatureEnabled;
|
||||
},
|
||||
isAllConversations() {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
<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>
|
||||
@@ -0,0 +1,79 @@
|
||||
<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>
|
||||
@@ -0,0 +1,74 @@
|
||||
<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>
|
||||
@@ -0,0 +1,41 @@
|
||||
<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>
|
||||
@@ -0,0 +1,28 @@
|
||||
<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>
|
||||
@@ -0,0 +1,86 @@
|
||||
<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>
|
||||
@@ -0,0 +1,172 @@
|
||||
<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>
|
||||
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
@@ -0,0 +1,86 @@
|
||||
<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>
|
||||
@@ -0,0 +1,71 @@
|
||||
<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>
|
||||
@@ -0,0 +1,219 @@
|
||||
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
@@ -0,0 +1,309 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
rightIcon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex relative items-center p-1.5 w-fit h-8 gap-1.5 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<slot name="leftIcon">
|
||||
<fluent-icon
|
||||
v-if="leftIcon"
|
||||
:icon="leftIcon"
|
||||
size="18"
|
||||
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</slot>
|
||||
<span
|
||||
v-if="buttonText"
|
||||
class="text-sm font-medium truncate text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</span>
|
||||
<slot name="rightIcon">
|
||||
<fluent-icon
|
||||
v-if="rightIcon"
|
||||
:icon="rightIcon"
|
||||
size="18"
|
||||
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
<slot name="dropdown" />
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center h-10 text-sm text-slate-500 dark:text-slate-300"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import ListItemButton from './DropdownListItemButton.vue';
|
||||
import DropdownSearch from './DropdownSearch.vue';
|
||||
import DropdownEmptyState from './DropdownEmptyState.vue';
|
||||
|
||||
const props = defineProps({
|
||||
listItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
enableSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeFilterId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
showClearFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const searchTerm = ref('');
|
||||
|
||||
const onSearch = value => {
|
||||
searchTerm.value = value;
|
||||
};
|
||||
|
||||
const filteredListItems = computed(() => {
|
||||
if (!searchTerm.value) return props.listItems;
|
||||
return picoSearch(props.listItems, searchTerm.value, ['name']);
|
||||
});
|
||||
|
||||
const isDropdownListEmpty = computed(() => {
|
||||
return !filteredListItems.value.length;
|
||||
});
|
||||
|
||||
const isFilterActive = id => {
|
||||
if (!props.activeFilterId) return false;
|
||||
return id === props.activeFilterId;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="absolute z-20 w-40 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-50 dark:border-slate-700/50 max-h-[400px]"
|
||||
@click.stop
|
||||
>
|
||||
<slot name="search">
|
||||
<dropdown-search
|
||||
v-if="enableSearch && listItems.length"
|
||||
:input-value="searchTerm"
|
||||
:input-placeholder="inputPlaceholder"
|
||||
:show-clear-filter="showClearFilter"
|
||||
@input="onSearch"
|
||||
@click="$emit('removeFilter')"
|
||||
/>
|
||||
</slot>
|
||||
<slot name="listItem">
|
||||
<dropdown-empty-state
|
||||
v-if="isDropdownListEmpty"
|
||||
:message="$t('REPORT.FILTER_ACTIONS.EMPTY_LIST')"
|
||||
/>
|
||||
<list-item-button
|
||||
v-for="item in filteredListItems"
|
||||
:key="item.id"
|
||||
:is-active="isFilterActive(item.id)"
|
||||
:button-text="item.name"
|
||||
@click="$emit('click', item)"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseenter="$emit('mouseenter')"
|
||||
@mouseleave="$emit('mouseleave')"
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
<div class="inline-flex items-center gap-3 overflow-hidden">
|
||||
<span
|
||||
class="text-sm font-medium truncate text-slate-900 dark:text-slate-50"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="isActive"
|
||||
icon="checkmark"
|
||||
size="18"
|
||||
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
</div>
|
||||
<slot name="dropdown" />
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
inputValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showClearFilter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-white z-10 dark:bg-slate-800 gap-2 px-3 border-b rounded-t-xl border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<div class="flex items-center w-full gap-2">
|
||||
<fluent-icon
|
||||
icon="search"
|
||||
size="18"
|
||||
class="text-slate-400 dark:text-slate-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full mb-0 text-sm bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-75 reset-base"
|
||||
:placeholder="inputPlaceholder"
|
||||
:value="inputValue"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<!-- Clear filter button -->
|
||||
<woot-button
|
||||
v-if="!inputValue && showClearFilter"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="primary"
|
||||
class="!px-1 !py-1.5"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
{{ $t('REPORT.FILTER_ACTIONS.CLEAR_FILTER') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<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>
|
||||
@@ -1,18 +1,25 @@
|
||||
<template>
|
||||
<div :class="labelClass" :style="labelStyle" :title="description">
|
||||
<div
|
||||
class="inline-flex ltr:mr-1 rtl:ml-1 mb-1"
|
||||
:class="labelClass"
|
||||
:style="labelStyle"
|
||||
:title="description"
|
||||
>
|
||||
<span v-if="icon" class="label-action--button">
|
||||
<fluent-icon :icon="icon" size="12" class="label--icon cursor-pointer" />
|
||||
</span>
|
||||
<span
|
||||
v-if="['smooth', 'dashed'].includes(variant) && title && !icon"
|
||||
:style="{ background: color }"
|
||||
class="label-color-dot"
|
||||
class="label-color-dot flex-shrink-0"
|
||||
/>
|
||||
<span v-if="!href">{{ title }}</span>
|
||||
<span v-if="!href" class="whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
{{ title }}
|
||||
</span>
|
||||
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
|
||||
<button
|
||||
v-if="showClose"
|
||||
class="label-close--button"
|
||||
class="label-close--button p-0"
|
||||
:style="{ color: textColor }"
|
||||
@click="onClick"
|
||||
>
|
||||
@@ -104,7 +111,7 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.label {
|
||||
@apply inline-flex items-center font-medium gap-1 mr-1 rtl:ml-1 rtl:mr-0 mb-1 p-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100 border border-solid border-slate-75 dark:border-slate-600 h-6;
|
||||
@apply items-center font-medium text-xs rounded-[4px] gap-1 p-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100 border border-solid border-slate-75 dark:border-slate-600 h-6;
|
||||
|
||||
&.small {
|
||||
@apply text-xs py-0.5 px-1 leading-tight h-5;
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
<template>
|
||||
<div class="preview-card--wrap" :class="{ 'active-card': active }">
|
||||
<div class="header--wrap" :class="{ active: active }">
|
||||
<div
|
||||
class="flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid border-slate-75 dark:border-slate-600"
|
||||
:class="{
|
||||
'bg-woot-25 dark:bg-slate-700 border border-solid border-woot-300 dark:border-woot-400':
|
||||
active,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center px-2 w-full h-10 bg-slate-50 dark:bg-slate-900 rounded-t-[5px] border-b border-solid border-slate-50 dark:border-slate-600"
|
||||
:class="{
|
||||
'bg-woot-50 border-b border-solid border-woot-75 dark:border-woot-700':
|
||||
active,
|
||||
}"
|
||||
>
|
||||
<div class="items-center flex font-medium p-1 text-sm">{{ heading }}</div>
|
||||
<fluent-icon
|
||||
v-if="active"
|
||||
@@ -56,21 +68,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-card--wrap {
|
||||
@apply flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid border-slate-75 dark:border-slate-600;
|
||||
|
||||
.header--wrap {
|
||||
@apply flex justify-between items-center px-2 w-full h-10 bg-slate-50 dark:bg-slate-900 rounded-t-[5px] border-b border-solid border-slate-50 dark:border-slate-600;
|
||||
}
|
||||
|
||||
.active {
|
||||
@apply bg-woot-50 border-b border-solid border-woot-75 dark:border-woot-700;
|
||||
}
|
||||
}
|
||||
|
||||
.active-card {
|
||||
@apply bg-woot-25 dark:bg-slate-700 border border-solid border-woot-300 dark:border-woot-400;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
class="toggle-button p-0"
|
||||
:class="{ active: value, small: size === 'small' }"
|
||||
role="switch"
|
||||
:aria-checked="value.toString()"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
>
|
||||
<a @click="onTabClick">
|
||||
{{ name }}
|
||||
<div v-if="showBadge" class="badge">
|
||||
<div v-if="showBadge" class="badge min-w-[20px]">
|
||||
<span>
|
||||
{{ getItemCount }}
|
||||
</span>
|
||||
|
||||
@@ -13,18 +13,23 @@
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<h3
|
||||
class="overflow-hidden whitespace-nowrap text-ellipsis leading-tight"
|
||||
class="text-slate-800 dark:text-slate-100 text-base font-medium pl-6 overflow-hidden whitespace-nowrap mb-1.5 text-ellipsis leading-tight"
|
||||
>
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
<span v-if="isOver(item)" class="completed">
|
||||
<span
|
||||
v-if="isOver(item)"
|
||||
class="text-green-500 dark:text-green-500 ml-1"
|
||||
>
|
||||
<fluent-icon icon="checkmark" />
|
||||
</span>
|
||||
</div>
|
||||
<span class="step">
|
||||
{{ items.indexOf(item) + 1 }}
|
||||
</span>
|
||||
<p>{{ item.body }}</p>
|
||||
<p class="text-slate-600 dark:text-slate-300 text-sm m-0 pl-6">
|
||||
{{ item.body }}
|
||||
</p>
|
||||
</div>
|
||||
</transition-group>
|
||||
</template>
|
||||
@@ -60,3 +65,40 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wizard-box {
|
||||
.item {
|
||||
@apply cursor-pointer after:bg-slate-75 before:bg-slate-75 dark:after:bg-slate-600 dark:before:bg-slate-600 py-4 pr-4 pl-6 relative before:h-4 before:top-0 last:before:h-0 first:before:h-0 last:after:h-0 before:content-[''] before:absolute before:w-0.5 after:content-[''] after:h-full after:absolute after:top-5 after:w-0.5;
|
||||
|
||||
&.active {
|
||||
h3 {
|
||||
@apply text-woot-500 dark:text-woot-500;
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.over {
|
||||
&::after {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
|
||||
& + .item {
|
||||
&::before {
|
||||
@apply bg-woot-500 dark:bg-woot-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step {
|
||||
@apply bg-slate-75 dark:bg-slate-600 rounded-2xl font-medium w-4 left-4 leading-4 z-[999] absolute text-center text-white dark:text-white text-xxs top-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,8 +40,7 @@ import AIAssistanceModal from './AIAssistanceModal.vue';
|
||||
import adminMixin from 'dashboard/mixins/aiMixin';
|
||||
import aiMixin from 'dashboard/mixins/isAdmin';
|
||||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
||||
|
||||
@@ -51,7 +50,7 @@ export default {
|
||||
AICTAModal,
|
||||
AIAssistanceCTAButton,
|
||||
},
|
||||
mixins: [aiMixin, eventListenerMixins, adminMixin, uiSettingsMixin],
|
||||
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
|
||||
data: () => ({
|
||||
showAIAssistanceModal: false,
|
||||
showAICtaModal: false,
|
||||
@@ -87,14 +86,17 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDownHandler(event) {
|
||||
const keyPattern = buildHotKeys(event);
|
||||
const shouldRevertTheContent =
|
||||
['meta+z', 'ctrl+z'].includes(keyPattern) && !!this.initialMessage;
|
||||
if (shouldRevertTheContent) {
|
||||
this.$emit('replace-text', this.initialMessage);
|
||||
this.initialMessage = '';
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'$mod+KeyZ': {
|
||||
action: () => {
|
||||
if (this.initialMessage) {
|
||||
this.$emit('replace-text', this.initialMessage);
|
||||
this.initialMessage = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
hideAIAssistanceModal() {
|
||||
this.recordAnalytics('DISMISS_AI_SUGGESTION', {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<div class="preview-item__wrap flex overflow-auto max-h-[12.5rem]">
|
||||
<div class="flex overflow-auto max-h-[12.5rem]">
|
||||
<div
|
||||
v-for="(attachment, index) in attachments"
|
||||
v-for="(attachment, index) in nonRecordedAudioAttachments"
|
||||
:key="attachment.id"
|
||||
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
|
||||
>
|
||||
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
|
||||
<img
|
||||
v-if="isTypeImage(attachment.resource)"
|
||||
class="image-thumb"
|
||||
class="object-cover w-6 h-6 rounded-sm"
|
||||
:src="attachment.thumb"
|
||||
/>
|
||||
<span v-else class="w-6 h-6 text-lg relative -top-px text-left">
|
||||
<span v-else class="relative w-6 h-6 text-lg text-left -top-px">
|
||||
📄
|
||||
</span>
|
||||
</div>
|
||||
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis">
|
||||
<div class="max-w-3/5 min-w-[50%] overflow-hidden text-ellipsis">
|
||||
<span
|
||||
class="h-4 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
@@ -23,73 +23,62 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-[30%] justify-center">
|
||||
<span
|
||||
class="item overflow-hidden text-xs text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
<span class="overflow-hidden text-xs text-ellipsis whitespace-nowrap">
|
||||
{{ formatFileSize(attachment.resource) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<woot-button
|
||||
v-if="!isTypeAudio(attachment.resource)"
|
||||
class="remove--attachment clear secondary"
|
||||
class="!w-6 !h-6 text-sm rounded-md hover:bg-slate-50 dark:hover:bg-slate-800 clear secondary"
|
||||
icon="dismiss"
|
||||
@click="() => onRemoveAttachment(index)"
|
||||
@click="onRemoveAttachment(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { formatBytes } from 'shared/helpers/FileHelper';
|
||||
export default {
|
||||
props: {
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
removeAttachment: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onRemoveAttachment(index) {
|
||||
this.removeAttachment(index);
|
||||
},
|
||||
formatFileSize(file) {
|
||||
const size = file.byte_size || file.size;
|
||||
return formatBytes(size, 0);
|
||||
},
|
||||
isTypeImage(file) {
|
||||
const type = file.content_type || file.type;
|
||||
return type.includes('image');
|
||||
},
|
||||
isTypeAudio(file) {
|
||||
const type = file.content_type || file.type;
|
||||
return type.includes('audio');
|
||||
},
|
||||
fileName(file) {
|
||||
return file.filename || file.name;
|
||||
},
|
||||
|
||||
const props = defineProps({
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['remove-attachment']);
|
||||
|
||||
const nonRecordedAudioAttachments = computed(() => {
|
||||
return props.attachments.filter(attachment => !attachment?.isRecordedAudio);
|
||||
});
|
||||
|
||||
const recordedAudioAttachments = computed(() =>
|
||||
props.attachments.filter(attachment => attachment.isRecordedAudio)
|
||||
);
|
||||
|
||||
const onRemoveAttachment = itemIndex => {
|
||||
emits(
|
||||
'remove-attachment',
|
||||
nonRecordedAudioAttachments.value
|
||||
.filter((_, index) => index !== itemIndex)
|
||||
.concat(recordedAudioAttachments.value)
|
||||
);
|
||||
};
|
||||
|
||||
const formatFileSize = file => {
|
||||
const size = file.byte_size || file.size;
|
||||
return formatBytes(size, 0);
|
||||
};
|
||||
|
||||
const isTypeImage = file => {
|
||||
const type = file.content_type || file.type;
|
||||
return type.includes('image');
|
||||
};
|
||||
|
||||
const fileName = file => {
|
||||
return file.filename || file.name;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.image-thumb {
|
||||
@apply w-6 h-6 object-cover rounded-sm;
|
||||
}
|
||||
|
||||
.file-name-wrap,
|
||||
.file-size-wrap {
|
||||
@apply flex items-center py-0 px-1;
|
||||
|
||||
> .item {
|
||||
@apply m-0 overflow-hidden text-xs font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.remove--attachment {
|
||||
@apply w-6 h-6 rounded-md text-sm cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<button
|
||||
class="header-section flex items-center text-base font-normal mr-4 ml-2 cursor-pointer text-woot-500 dark:text-woot-500"
|
||||
class="header-section flex items-center text-base font-normal mr-4 ml-2 p-0 cursor-pointer text-woot-500 dark:text-woot-500"
|
||||
@click.capture="goBack"
|
||||
>
|
||||
<fluent-icon icon="chevron-left" />
|
||||
|
||||
@@ -10,11 +10,10 @@
|
||||
</template>
|
||||
<script>
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
@@ -31,14 +30,18 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndNKey(e)) {
|
||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
this.onTabChange(0);
|
||||
} else {
|
||||
this.onTabChange(this.activeTabIndex + 1);
|
||||
}
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
'Alt+KeyN': {
|
||||
action: () => {
|
||||
if (this.activeTab === wootConstants.ASSIGNEE_TYPE.ALL) {
|
||||
this.onTabChange(0);
|
||||
} else {
|
||||
this.onTabChange(this.activeTabIndex + 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
onTabChange(selectedTabIndex) {
|
||||
if (this.items[selectedTabIndex].key !== this.activeTab) {
|
||||
|
||||
@@ -18,13 +18,11 @@
|
||||
|
||||
<script>
|
||||
import { Chrome } from 'vue-color';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Chrome,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="empty-state py-16 px-1 ml-0 mr-0">
|
||||
<h3
|
||||
v-if="title"
|
||||
class="text-slate-700 dark:text-slate-200 block text-center w-full text-4xl font-thin"
|
||||
class="text-slate-700 dark:text-slate-200 block text-center w-full text-xl font-thin"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
@@ -15,6 +15,7 @@
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="inbox--name inline-flex items-center py-0.5 px-0 leading-3 font-medium bg-none text-slate-600 dark:text-slate-500 text-xs my-0 mx-2.5"
|
||||
class="inbox--name inline-flex items-center py-0.5 px-0 leading-3 whitespace-nowrap font-medium bg-none text-slate-600 dark:text-slate-500 text-xs my-0 mx-2.5"
|
||||
>
|
||||
<fluent-icon
|
||||
class="mr-0.5 rtl:ml-0.5 rtl:mr-0"
|
||||
|
||||
@@ -31,15 +31,9 @@
|
||||
|
||||
<script>
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
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';
|
||||
import {
|
||||
buildHotKeys,
|
||||
isEscape,
|
||||
isActiveElementTypeable,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -47,7 +41,7 @@ export default {
|
||||
LabelDropdown,
|
||||
},
|
||||
|
||||
mixins: [clickaway, adminMixin, eventListenerMixins],
|
||||
mixins: [adminMixin, keyboardEventListenerMixins],
|
||||
|
||||
props: {
|
||||
allLabels: {
|
||||
@@ -88,17 +82,19 @@ export default {
|
||||
closeDropdownLabel() {
|
||||
this.showSearchDropdownLabel = false;
|
||||
},
|
||||
|
||||
handleKeyEvents(e) {
|
||||
const keyPattern = buildHotKeys(e);
|
||||
|
||||
if (keyPattern === 'l' && !isActiveElementTypeable(e)) {
|
||||
this.toggleLabels();
|
||||
e.preventDefault();
|
||||
} else if (isEscape(e) && this.showSearchDropdownLabel) {
|
||||
this.closeDropdownLabel();
|
||||
e.preventDefault();
|
||||
}
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
KeyL: {
|
||||
action: e => {
|
||||
this.toggleLabels();
|
||||
e.preventDefault();
|
||||
},
|
||||
},
|
||||
Escape: {
|
||||
action: () => this.closeDropdownLabel(),
|
||||
allowOnFocusedInput: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center p-8">
|
||||
<h6
|
||||
class="block text-base text-center w-100 text-slate-800 dark:text-slate-300"
|
||||
class="flex items-center gap-2 text-base text-center w-100 text-slate-800 dark:text-slate-300"
|
||||
>
|
||||
<span class="mr-3">{{ message }}</span>
|
||||
<span class="text-base font-medium text-slate-800 dark:text-slate-100">
|
||||
{{ message }}
|
||||
</span>
|
||||
<span class="spinner" />
|
||||
</h6>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user