Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad834f677 | ||
|
|
17f66f6f9b | ||
|
|
5e2fb59302 | ||
|
|
3ceb27c575 | ||
|
|
220a947380 | ||
|
|
8505aa48c3 | ||
|
|
b49eaa5c45 | ||
|
|
f18ed01eb7 | ||
|
|
aa5fa0c758 | ||
|
|
42eca69763 | ||
|
|
3a78192e74 |
@@ -1,6 +1,15 @@
|
||||
class Google::CallbacksController < OauthCallbackController
|
||||
include GoogleConcern
|
||||
|
||||
def find_channel_by_email
|
||||
# find by imap_login first, and then by email
|
||||
# this ensures the legacy users can migrate correctly even if inbox email address doesn't match
|
||||
imap_channel = Channel::Email.find_by(imap_login: users_data['email'], account: account)
|
||||
return imap_channel if imap_channel
|
||||
|
||||
Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def provider_name
|
||||
|
||||
@@ -25,7 +25,7 @@ class OauthCallbackController < ApplicationController
|
||||
end
|
||||
|
||||
def find_or_create_inbox
|
||||
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
channel_email = find_channel_by_email
|
||||
# we need this value to know where to redirect on sucessful processing of the callback
|
||||
channel_exists = channel_email.present?
|
||||
|
||||
@@ -39,6 +39,10 @@ class OauthCallbackController < ApplicationController
|
||||
[channel_email.inbox, channel_exists]
|
||||
end
|
||||
|
||||
def find_channel_by_email
|
||||
Channel::Email.find_by(email: users_data['email'], account: account)
|
||||
end
|
||||
|
||||
def update_channel(channel_email)
|
||||
channel_email.update!({
|
||||
imap_login: users_data['email'], imap_address: imap_address,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useStore } from 'dashboard/composables/store';
|
||||
import WootSnackbarBox from './components/SnackbarContainer.vue';
|
||||
import { setColorTheme } from './helper/themeHelper';
|
||||
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
@@ -35,8 +36,9 @@ export default {
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { accountId } = useAccount();
|
||||
|
||||
return { router, store };
|
||||
return { router, store, currentAccountId: accountId };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -52,7 +54,6 @@ export default {
|
||||
currentUser: 'getCurrentUser',
|
||||
authUIFlags: 'getAuthUIFlags',
|
||||
accountUIFlags: 'accounts/getUIFlags',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
hasAccounts() {
|
||||
const { accounts = [] } = this.currentUser || {};
|
||||
@@ -69,10 +70,13 @@ export default {
|
||||
this.showAddAccountModal = true;
|
||||
}
|
||||
},
|
||||
currentAccountId() {
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
}
|
||||
currentAccountId: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
if (this.currentAccountId) {
|
||||
this.initializeAccount();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getSidebarItems } from './config/default-sidebar';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
@@ -33,6 +34,7 @@ export default {
|
||||
setup(props, { emit }) {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { accountId } = useAccount();
|
||||
|
||||
const toggleKeyShortcutModal = () => {
|
||||
emit('openKeyShortcutModal');
|
||||
@@ -72,6 +74,7 @@ export default {
|
||||
|
||||
return {
|
||||
toggleKeyShortcutModal,
|
||||
accountId,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -82,7 +85,6 @@ export default {
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
|
||||
@@ -90,13 +90,17 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-end pb-6">
|
||||
<PrimaryNavItem
|
||||
<a
|
||||
v-if="!isACustomBrandedInstance"
|
||||
icon="book-open-globe"
|
||||
name="DOCS"
|
||||
open-in-new-page
|
||||
:to="helpDocsURL"
|
||||
/>
|
||||
v-tooltip.right="$t(`SIDEBAR.DOCS`)"
|
||||
:href="helpDocsURL"
|
||||
class="text-slate-700 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"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<fluent-icon icon="book-open-globe" />
|
||||
<span class="sr-only">{{ $t(`SIDEBAR.DOCS`) }}</span>
|
||||
</a>
|
||||
<NotificationBell @open-notification-panel="openNotificationPanel" />
|
||||
<AgentDetails @toggle-menu="toggleOptions" />
|
||||
<OptionsMenu
|
||||
|
||||
@@ -48,7 +48,7 @@ const debouncedEmit = debounce(value => {
|
||||
|
||||
const onSearch = value => {
|
||||
searchTerm.value = value;
|
||||
debouncedEmit();
|
||||
debouncedEmit(value);
|
||||
};
|
||||
|
||||
const filteredListItems = computed(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import Letter from 'vue-letter';
|
||||
import { Letter } from 'vue-letter';
|
||||
import GalleryView from '../components/GalleryView.vue';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { ref } from 'vue';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useAccount } from '../useAccount';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
vi.mock('dashboard/composables/store');
|
||||
vi.mock('vue-router');
|
||||
|
||||
describe('useAccount', () => {
|
||||
beforeEach(() => {
|
||||
useStoreGetters.mockReturnValue({
|
||||
getCurrentAccountId: ref(123),
|
||||
useRoute.mockReturnValue({
|
||||
params: {
|
||||
accountId: 123,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { computed } from 'vue';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
/**
|
||||
* Composable for account-related operations.
|
||||
* @returns {Object} An object containing account-related properties and methods.
|
||||
*/
|
||||
export function useAccount() {
|
||||
const getters = useStoreGetters();
|
||||
|
||||
/**
|
||||
* Computed property for the current account ID.
|
||||
* @type {import('vue').ComputedRef<number>}
|
||||
*/
|
||||
const accountId = computed(() => getters.getCurrentAccountId.value);
|
||||
const route = useRoute();
|
||||
|
||||
const accountId = computed(() => {
|
||||
return Number(route.params.accountId);
|
||||
});
|
||||
|
||||
/**
|
||||
* Generates an account-scoped URL.
|
||||
|
||||
@@ -5,6 +5,7 @@ import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import semver from 'semver';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
@@ -13,9 +14,10 @@ export default {
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
const { enabledLanguages } = useConfig();
|
||||
const { accountId } = useAccount();
|
||||
const v$ = useVuelidate();
|
||||
|
||||
return { updateUISettings, v$, enabledLanguages };
|
||||
return { updateUISettings, v$, enabledLanguages, accountId };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -46,7 +48,6 @@ export default {
|
||||
globalConfig: 'globalConfig/get',
|
||||
getAccount: 'accounts/getAccount',
|
||||
uiFlags: 'accounts/getUIFlags',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
showAutoResolutionConfig() {
|
||||
|
||||
@@ -47,7 +47,7 @@ const emailProviderList = computed(() => {
|
||||
|
||||
function onClick(emailProvider) {
|
||||
if (emailProvider.isEnabled) {
|
||||
this.provider = emailProvider.key;
|
||||
provider.value = emailProvider.key;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import InboxReconnectionRequired from '../../components/InboxReconnectionRequired.vue';
|
||||
import googleClient from 'dashboard/api/channel/googleClient';
|
||||
|
||||
@@ -17,11 +17,18 @@ const { t } = useI18n();
|
||||
|
||||
const isRequestingAuthorization = ref(false);
|
||||
|
||||
const inboxEmail = computed(() => {
|
||||
if (props.inbox.imap_login && props.inbox.imap_enabled) {
|
||||
return props.inbox.imap_login;
|
||||
}
|
||||
return props.inbox.email;
|
||||
});
|
||||
|
||||
async function requestAuthorization() {
|
||||
try {
|
||||
isRequestingAuthorization.value = true;
|
||||
const response = await googleClient.generateAuthorization({
|
||||
email: props.inbox.email,
|
||||
email: inboxEmail.value,
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ export default {
|
||||
"
|
||||
data-testid="app-title"
|
||||
@input="v$.app.title.$touch"
|
||||
@blur="v$.slug.$touch"
|
||||
@blur="v$.app.title.$touch"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="app.content.url"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import types from '../../mutation-types';
|
||||
import * as Sentry from '@sentry/vue';
|
||||
|
||||
export const mutations = {
|
||||
[types.SET_CONTACT_UI_FLAG]($state, data) {
|
||||
@@ -48,12 +49,23 @@ export const mutations = {
|
||||
[types.DELETE_CONTACT]: ($state, id) => {
|
||||
const index = $state.sortOrder.findIndex(item => item === id);
|
||||
$state.sortOrder.splice(index, 1);
|
||||
$state.records[id] = null;
|
||||
delete $state.records[id];
|
||||
},
|
||||
|
||||
[types.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
|
||||
Object.values($state.records).forEach(element => {
|
||||
const availabilityStatus = data[element.id];
|
||||
let availabilityStatus;
|
||||
try {
|
||||
availabilityStatus = data[element.id];
|
||||
} catch (error) {
|
||||
Sentry.setContext('contact is undefined', {
|
||||
records: $state.records,
|
||||
data: data,
|
||||
});
|
||||
Sentry.captureException(error);
|
||||
|
||||
return;
|
||||
}
|
||||
if (availabilityStatus) {
|
||||
$state.records[element.id].availability_status = availabilityStatus;
|
||||
} else {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const mutations = {
|
||||
// On reconnect, if there is existing notification with same primary_actor_id,
|
||||
// it will be deleted and the new one will be added. So it will solve with duplicate notification
|
||||
if (existingNotification) {
|
||||
$state.records[existingNotification.id] = undefined;
|
||||
delete $state.records[existingNotification.id];
|
||||
}
|
||||
|
||||
$state.records[notification.id] = {
|
||||
@@ -72,7 +72,7 @@ export const mutations = {
|
||||
},
|
||||
[types.DELETE_NOTIFICATION]($state, data) {
|
||||
const { notification, unread_count: unreadCount, count } = data;
|
||||
$state.records[notification.id] = undefined;
|
||||
delete $state.records[notification.id];
|
||||
$state.meta.unreadCount = unreadCount;
|
||||
$state.meta.count = count;
|
||||
},
|
||||
@@ -83,7 +83,7 @@ export const mutations = {
|
||||
[types.DELETE_READ_NOTIFICATIONS]: $state => {
|
||||
Object.values($state.records).forEach(item => {
|
||||
if (item.read_at) {
|
||||
$state.records[item.id] = undefined;
|
||||
delete $state.records[item.id];
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
@import 'dashboard/assets/scss/app';
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
--_histoire-color-primary-50: 235 245 255;
|
||||
--_histoire-color-primary-100: 194 225 255;
|
||||
--_histoire-color-primary-200: 153 206 255;
|
||||
--_histoire-color-primary-300: 112 186 255;
|
||||
--_histoire-color-primary-400: 71 166 255;
|
||||
--_histoire-color-primary-500: 31 147 255;
|
||||
--_histoire-color-primary-600: 25 118 204;
|
||||
--_histoire-color-primary-700: 19 88 153;
|
||||
--_histoire-color-primary-800: 12 59 102;
|
||||
--_histoire-color-primary-900: 6 29 51;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 59.1 (86144) - https://sketch.com -->
|
||||
<title>woot-log</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="woot-log" fill-rule="nonzero">
|
||||
<circle id="Oval" fill="#47A7F6" cx="256" cy="256" r="256"></circle>
|
||||
<path d="M362.807947,368.807947 L244.122956,368.807947 C178.699407,368.807947 125.456954,315.561812 125.456954,250.12177 C125.456954,184.703089 178.699407,131.456954 244.124143,131.456954 C309.565494,131.456954 362.807947,184.703089 362.807947,250.12177 L362.807947,368.807947 Z" id="Fill-1" stroke="#FFFFFF" stroke-width="6" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 916 B |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -10,7 +10,6 @@ import Multiselect from 'vue-multiselect';
|
||||
import { plugin, defaultConfig } from '@formkit/vue';
|
||||
import WootSwitch from 'components/ui/Switch.vue';
|
||||
import WootWizard from 'components/ui/Wizard.vue';
|
||||
import { sync } from 'vuex-router-sync';
|
||||
import FloatingVue from 'floating-vue';
|
||||
import WootUiKit from 'dashboard/components';
|
||||
import App from 'dashboard/App.vue';
|
||||
@@ -18,6 +17,7 @@ import i18nMessages from 'dashboard/i18n';
|
||||
import createAxios from 'dashboard/helper/APIHelper';
|
||||
|
||||
import commonHelpers, { isJSONValid } from 'dashboard/helper/commons';
|
||||
import { sync } from 'vuex-router-sync';
|
||||
import router, { initalizeRouter } from 'dashboard/routes';
|
||||
import store from 'dashboard/store';
|
||||
import constants from 'dashboard/constants/globals';
|
||||
@@ -42,6 +42,8 @@ const i18n = createI18n({
|
||||
messages: i18nMessages,
|
||||
});
|
||||
|
||||
sync(store, router);
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(i18n);
|
||||
app.use(store);
|
||||
@@ -97,7 +99,6 @@ app.component('fluent-icon', FluentIcon);
|
||||
app.directive('resize', vResizeObserver);
|
||||
app.directive('on-clickaway', onClickaway);
|
||||
|
||||
sync(store, router);
|
||||
// load common helpers into js
|
||||
commonHelpers();
|
||||
window.WOOT_STORE = store;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import './design-system/histoire.scss';
|
||||
@@ -0,0 +1,43 @@
|
||||
import { defineConfig } from 'histoire';
|
||||
import { HstVue } from '@histoire/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
setupFile: './histoire.setup.ts',
|
||||
plugins: [HstVue()],
|
||||
vite: {
|
||||
server: {
|
||||
port: 6179,
|
||||
},
|
||||
},
|
||||
viteIgnorePlugins: ['vite-plugin-ruby'],
|
||||
theme: {
|
||||
darkClass: 'dark',
|
||||
title: '@chatwoot/design',
|
||||
logo: {
|
||||
square: './design-system/images/logo-thumbnail.svg',
|
||||
light: './design-system/images/logo.png',
|
||||
dark: './design-system/images/logo-dark.png',
|
||||
},
|
||||
},
|
||||
defaultStoryProps: {
|
||||
icon: 'carbon:cube',
|
||||
iconColor: '#1F93FF',
|
||||
layout: {
|
||||
type: 'grid',
|
||||
width: '80%',
|
||||
},
|
||||
},
|
||||
tree: {
|
||||
groups: [
|
||||
{
|
||||
id: 'top',
|
||||
title: '',
|
||||
},
|
||||
{
|
||||
id: 'components',
|
||||
title: 'Components',
|
||||
include: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
+6
-1
@@ -14,7 +14,10 @@
|
||||
"ruby:prettier": "bundle exec rubocop -a",
|
||||
"build:sdk": "BUILD_MODE=library vite build",
|
||||
"prepare": "husky install",
|
||||
"size": "size-limit"
|
||||
"size": "size-limit",
|
||||
"story:dev": "histoire dev",
|
||||
"story:build": "histoire build",
|
||||
"story:preview": "histoire preview"
|
||||
},
|
||||
"size-limit": [
|
||||
{
|
||||
@@ -97,6 +100,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@egoist/tailwindcss-icons": "^1.8.1",
|
||||
"@histoire/plugin-vue": "0.17.15",
|
||||
"@iconify-json/logos": "^1.2.0",
|
||||
"@iconify-json/lucide": "^1.2.5",
|
||||
"@size-limit/file": "^8.2.4",
|
||||
@@ -113,6 +117,7 @@
|
||||
"eslint-plugin-vitest-globals": "^1.5.0",
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"histoire": "0.17.15",
|
||||
"husky": "^7.0.0",
|
||||
"jsdom": "^24.1.3",
|
||||
"lint-staged": "14.0.1",
|
||||
|
||||
Generated
+953
-3
File diff suppressed because it is too large
Load Diff
@@ -39,8 +39,9 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
expect(Redis::Alfred.get(cache_key)).to be_nil
|
||||
end
|
||||
|
||||
it 'creates updates inbox channel config if inbox exists and authentication is successful' do
|
||||
inbox = create(:channel_email, account: account, email: email)&.inbox
|
||||
it 'updates inbox channel config if inbox exists with imap_login and authentication is successful' do
|
||||
channel_email = create(:channel_email, account: account, imap_login: email)
|
||||
inbox = channel_email.inbox
|
||||
expect(inbox.channel.provider_config).to eq({})
|
||||
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
|
||||
@@ -50,7 +51,7 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
|
||||
get google_callback_url, params: { code: code }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
|
||||
+7
-1
@@ -42,6 +42,11 @@ if (isLibraryMode) {
|
||||
plugins = [vue(vueOptions)];
|
||||
}
|
||||
|
||||
const esbuildOptions = {
|
||||
minifyIdentifiers: false,
|
||||
keepNames: true,
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: plugins,
|
||||
build: {
|
||||
@@ -66,7 +71,7 @@ export default defineConfig({
|
||||
lib: isLibraryMode
|
||||
? {
|
||||
entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
|
||||
formats: ['umd'], // UMD format for single file
|
||||
formats: ['cjs'], // CJS format for single file
|
||||
name: 'sdk',
|
||||
}
|
||||
: undefined,
|
||||
@@ -107,4 +112,5 @@ export default defineConfig({
|
||||
mockReset: true,
|
||||
clearMocks: true,
|
||||
},
|
||||
esbuild: isLibraryMode ? esbuildOptions : undefined,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user