feat(frontend): add Shangwutong inbox presence actions
This commit is contained in:
@@ -1190,6 +1190,15 @@
|
||||
"AWAY": "Away",
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"QUICK_PRESENCE": {
|
||||
"SELECT_ALL": "Select all Shangwutong inboxes",
|
||||
"SELECT_INBOX": "Select this Shangwutong inbox",
|
||||
"SELECTED": "Selected {count}",
|
||||
"SET_ONLINE": "Set selected online",
|
||||
"SET_OFFLINE": "Set selected offline",
|
||||
"SUCCESS": "Set {count} Shangwutong inbox(es) to {presence}",
|
||||
"ERROR": "Updated {success} Shangwutong inbox(es); {failed} failed. Please try again."
|
||||
},
|
||||
"STATUS_FIELDS": {
|
||||
"CONNECTION_STATUS": "Connection status",
|
||||
"ACTUAL_PRESENCE": "Current presence",
|
||||
|
||||
@@ -1190,6 +1190,15 @@
|
||||
"AWAY": "离开",
|
||||
"OFFLINE": "离线"
|
||||
},
|
||||
"QUICK_PRESENCE": {
|
||||
"SELECT_ALL": "全选商务通收件箱",
|
||||
"SELECT_INBOX": "选择此商务通收件箱",
|
||||
"SELECTED": "已选择 {count} 个",
|
||||
"SET_ONLINE": "批量上线",
|
||||
"SET_OFFLINE": "批量下线",
|
||||
"SUCCESS": "已将 {count} 个商务通收件箱切换为{presence}",
|
||||
"ERROR": "已更新 {success} 个商务通收件箱,{failed} 个失败,请重试"
|
||||
},
|
||||
"STATUS_FIELDS": {
|
||||
"CONNECTION_STATUS": "连接状态",
|
||||
"ACTUAL_PRESENCE": "当前在线状态",
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Index from './Index.vue';
|
||||
|
||||
const mocked = vi.hoisted(() => ({
|
||||
alert: vi.fn(),
|
||||
dispatch: vi.fn(),
|
||||
inboxes: [],
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: key => key }),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/composables', () => ({
|
||||
useAlert: (...args) => mocked.alert(...args),
|
||||
}));
|
||||
|
||||
vi.mock('dashboard/composables/useAdmin', async () => {
|
||||
const { computed } = await vi.importActual('vue');
|
||||
return {
|
||||
useAdmin: () => ({ isAdmin: computed(() => true) }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('dashboard/composables/store', async () => {
|
||||
const { computed } = await vi.importActual('vue');
|
||||
return {
|
||||
useMapGetter: () => computed(() => mocked.inboxes),
|
||||
useStoreGetters: () => ({
|
||||
'inboxes/getUIFlags': computed(() => ({ isFetching: false })),
|
||||
}),
|
||||
useStore: () => ({ dispatch: mocked.dispatch }),
|
||||
};
|
||||
});
|
||||
|
||||
const mountComponent = () =>
|
||||
shallowMount(Index, {
|
||||
global: {
|
||||
mocks: { $t: key => key },
|
||||
stubs: {
|
||||
SettingsLayout: { template: '<div><slot /></div>' },
|
||||
BaseSettingsHeader: true,
|
||||
ChannelName: true,
|
||||
ChannelIcon: true,
|
||||
Avatar: true,
|
||||
Button: true,
|
||||
Checkbox: true,
|
||||
BulkSelectBar: true,
|
||||
RouterLink: true,
|
||||
'woot-confirm-delete-modal': true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('inbox settings list', () => {
|
||||
beforeEach(() => {
|
||||
mocked.alert.mockReset();
|
||||
mocked.dispatch.mockReset().mockResolvedValue({});
|
||||
mocked.inboxes = [
|
||||
{
|
||||
id: 1,
|
||||
name: '商务通在线',
|
||||
channel_type: 'Channel::Shangwutong',
|
||||
desired_presence: 'online',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '商务通离线',
|
||||
channel_type: 'Channel::Shangwutong',
|
||||
desired_presence: 'offline',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'API',
|
||||
channel_type: 'Channel::Api',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
it('updates selected Shangwutong inboxes in one batch and skips unchanged ones', async () => {
|
||||
const wrapper = mountComponent();
|
||||
|
||||
wrapper.vm.toggleInboxSelection(1);
|
||||
wrapper.vm.toggleInboxSelection(2);
|
||||
await wrapper.vm.updateSelectedPresence('offline');
|
||||
|
||||
expect(mocked.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(mocked.dispatch).toHaveBeenCalledWith('inboxes/updateInbox', {
|
||||
id: 1,
|
||||
formData: false,
|
||||
channel: { desired_presence: 'offline' },
|
||||
});
|
||||
expect(mocked.alert).toHaveBeenCalledWith(
|
||||
'INBOX_MGMT.SHANGWUTONG_SETTINGS.QUICK_PRESENCE.SUCCESS'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { picoSearch } from '@scmmishra/pico-search';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import {
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
import ChannelName from './components/ChannelName.vue';
|
||||
import ChannelIcon from 'next/icon/ChannelIcon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
@@ -37,6 +40,108 @@ const filteredInboxesList = computed(() => {
|
||||
return picoSearch(inboxesList.value, query, ['name', 'channel_type']);
|
||||
});
|
||||
|
||||
const selectedInboxIds = ref(new Set());
|
||||
const updatingPresenceIds = ref(new Set());
|
||||
|
||||
const isShangwutongInbox = inbox =>
|
||||
inbox.channel_type === INBOX_TYPES.SHANGWUTONG;
|
||||
const inboxDesiredPresence = inbox => inbox.desired_presence || 'online';
|
||||
const getPresenceLabel = presence =>
|
||||
presence === 'online'
|
||||
? t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.ONLINE')
|
||||
: t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.OFFLINE');
|
||||
|
||||
const shangwutongInboxes = computed(() =>
|
||||
(inboxesList.value || []).filter(isShangwutongInbox)
|
||||
);
|
||||
const visibleShangwutongInboxes = computed(() =>
|
||||
(filteredInboxesList.value || []).filter(isShangwutongInbox)
|
||||
);
|
||||
const selectedShangwutongInboxes = computed(() =>
|
||||
shangwutongInboxes.value.filter(inbox => selectedInboxIds.value.has(inbox.id))
|
||||
);
|
||||
const isUpdatingAnyPresence = computed(
|
||||
() => updatingPresenceIds.value.size > 0
|
||||
);
|
||||
|
||||
const isInboxSelected = inboxId => selectedInboxIds.value.has(inboxId);
|
||||
const isPresenceUpdating = inboxId => updatingPresenceIds.value.has(inboxId);
|
||||
const allSelectedHavePresence = presence =>
|
||||
selectedShangwutongInboxes.value.length > 0 &&
|
||||
selectedShangwutongInboxes.value.every(
|
||||
inbox => inboxDesiredPresence(inbox) === presence
|
||||
);
|
||||
|
||||
const toggleInboxSelection = inboxId => {
|
||||
const nextSelection = new Set(selectedInboxIds.value);
|
||||
if (nextSelection.has(inboxId)) {
|
||||
nextSelection.delete(inboxId);
|
||||
} else {
|
||||
nextSelection.add(inboxId);
|
||||
}
|
||||
selectedInboxIds.value = nextSelection;
|
||||
};
|
||||
|
||||
const updateInboxPresence = async (targetInboxes, presence) => {
|
||||
const inboxesToUpdate = targetInboxes.filter(
|
||||
inbox =>
|
||||
isShangwutongInbox(inbox) && inboxDesiredPresence(inbox) !== presence
|
||||
);
|
||||
if (!inboxesToUpdate.length) return;
|
||||
|
||||
const nextUpdatingIds = new Set(updatingPresenceIds.value);
|
||||
inboxesToUpdate.forEach(inbox => nextUpdatingIds.add(inbox.id));
|
||||
updatingPresenceIds.value = nextUpdatingIds;
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
inboxesToUpdate.map(inbox =>
|
||||
Promise.resolve().then(() =>
|
||||
store.dispatch('inboxes/updateInbox', {
|
||||
id: inbox.id,
|
||||
formData: false,
|
||||
channel: { desired_presence: presence },
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const completedIds = new Set(
|
||||
inboxesToUpdate
|
||||
.filter((_, index) => results[index].status === 'fulfilled')
|
||||
.map(inbox => inbox.id)
|
||||
);
|
||||
selectedInboxIds.value = new Set(
|
||||
[...selectedInboxIds.value].filter(id => !completedIds.has(id))
|
||||
);
|
||||
|
||||
const remainingUpdatingIds = new Set(updatingPresenceIds.value);
|
||||
inboxesToUpdate.forEach(inbox => remainingUpdatingIds.delete(inbox.id));
|
||||
updatingPresenceIds.value = remainingUpdatingIds;
|
||||
|
||||
const failedCount = results.filter(
|
||||
result => result.status === 'rejected'
|
||||
).length;
|
||||
if (failedCount) {
|
||||
useAlert(
|
||||
t('INBOX_MGMT.SHANGWUTONG_SETTINGS.QUICK_PRESENCE.ERROR', {
|
||||
success: inboxesToUpdate.length - failedCount,
|
||||
failed: failedCount,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
useAlert(
|
||||
t('INBOX_MGMT.SHANGWUTONG_SETTINGS.QUICK_PRESENCE.SUCCESS', {
|
||||
count: inboxesToUpdate.length,
|
||||
presence: getPresenceLabel(presence),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateSelectedPresence = presence =>
|
||||
updateInboxPresence(selectedShangwutongInboxes.value, presence);
|
||||
|
||||
const uiFlags = computed(() => getters['inboxes/getUIFlags'].value);
|
||||
|
||||
const deleteConfirmText = computed(
|
||||
@@ -60,6 +165,9 @@ const confirmPlaceHolderText = computed(
|
||||
const deleteInbox = async ({ id }) => {
|
||||
try {
|
||||
await store.dispatch('inboxes/delete', id);
|
||||
const nextSelection = new Set(selectedInboxIds.value);
|
||||
nextSelection.delete(id);
|
||||
selectedInboxIds.value = nextSelection;
|
||||
useAlert(t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
@@ -108,6 +216,54 @@ const openDelete = inbox => {
|
||||
</BaseSettingsHeader>
|
||||
</template>
|
||||
<template #body>
|
||||
<BulkSelectBar
|
||||
v-if="isAdmin && selectedInboxIds.size"
|
||||
v-model="selectedInboxIds"
|
||||
:all-items="visibleShangwutongInboxes"
|
||||
:select-all-label="
|
||||
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.QUICK_PRESENCE.SELECT_ALL')
|
||||
"
|
||||
:selected-count-label="
|
||||
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.QUICK_PRESENCE.SELECTED', {
|
||||
count: selectedInboxIds.size,
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #actions>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
:label="
|
||||
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.QUICK_PRESENCE.SET_ONLINE')
|
||||
"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="teal"
|
||||
:disabled="
|
||||
!selectedShangwutongInboxes.length ||
|
||||
isUpdatingAnyPresence ||
|
||||
allSelectedHavePresence('online')
|
||||
"
|
||||
:is-loading="isUpdatingAnyPresence"
|
||||
@click="updateSelectedPresence('online')"
|
||||
/>
|
||||
<Button
|
||||
:label="
|
||||
$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.QUICK_PRESENCE.SET_OFFLINE')
|
||||
"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="slate"
|
||||
:disabled="
|
||||
!selectedShangwutongInboxes.length ||
|
||||
isUpdatingAnyPresence ||
|
||||
allSelectedHavePresence('offline')
|
||||
"
|
||||
:is-loading="isUpdatingAnyPresence"
|
||||
@click="updateSelectedPresence('offline')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BulkSelectBar>
|
||||
<span
|
||||
v-if="!filteredInboxesList.length && searchQuery"
|
||||
class="flex-1 flex items-center justify-center py-20 text-center text-body-main !text-base text-n-slate-11"
|
||||
@@ -121,6 +277,17 @@ const openDelete = inbox => {
|
||||
class="flex justify-between flex-row items-start gap-4 py-4"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<Checkbox
|
||||
v-if="isAdmin && isShangwutongInbox(inbox)"
|
||||
:model-value="isInboxSelected(inbox.id)"
|
||||
:disabled="isUpdatingAnyPresence"
|
||||
:title="
|
||||
$t(
|
||||
'INBOX_MGMT.SHANGWUTONG_SETTINGS.QUICK_PRESENCE.SELECT_INBOX'
|
||||
)
|
||||
"
|
||||
@change="toggleInboxSelection(inbox.id)"
|
||||
/>
|
||||
<div
|
||||
v-if="inbox.avatar_url"
|
||||
class="bg-n-alpha-3 rounded-xl size-10 ring ring-n-solid-1 border border-n-strong shadow-sm grid place-items-center"
|
||||
@@ -150,7 +317,36 @@ const openDelete = inbox => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<div class="flex flex-wrap gap-3 justify-end">
|
||||
<div
|
||||
v-if="isAdmin && isShangwutongInbox(inbox)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Button
|
||||
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.ONLINE')"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="teal"
|
||||
:disabled="
|
||||
isUpdatingAnyPresence ||
|
||||
inboxDesiredPresence(inbox) === 'online'
|
||||
"
|
||||
:is-loading="isPresenceUpdating(inbox.id)"
|
||||
@click.stop="updateInboxPresence([inbox], 'online')"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.PRESENCE.OFFLINE')"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
color="slate"
|
||||
:disabled="
|
||||
isUpdatingAnyPresence ||
|
||||
inboxDesiredPresence(inbox) === 'offline'
|
||||
"
|
||||
:is-loading="isPresenceUpdating(inbox.id)"
|
||||
@click.stop="updateInboxPresence([inbox], 'offline')"
|
||||
/>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'settings_inbox_show',
|
||||
|
||||
Reference in New Issue
Block a user