feat: Allow users to disable marking offline automatically (#6079)
Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
parent
82d3398932
commit
aaacf9d4d2
14 changed files with 188 additions and 14 deletions
|
@ -18,6 +18,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_offline
|
||||||
|
@user.account_users.find_by!(account_id: auto_offline_params[:account_id]).update!(auto_offline: auto_offline_params[:auto_offline] || false)
|
||||||
|
end
|
||||||
|
|
||||||
def availability
|
def availability
|
||||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||||
end
|
end
|
||||||
|
@ -37,6 +41,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||||
params.require(:profile).permit(:account_id, :availability)
|
params.require(:profile).permit(:account_id, :availability)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_offline_params
|
||||||
|
params.require(:profile).permit(:account_id, :auto_offline)
|
||||||
|
end
|
||||||
|
|
||||||
def profile_params
|
def profile_params
|
||||||
params.require(:profile).permit(
|
params.require(:profile).permit(
|
||||||
:email,
|
:email,
|
||||||
|
|
|
@ -144,6 +144,12 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateAutoOffline(accountId, autoOffline = false) {
|
||||||
|
return axios.post(endPoints('autoOffline').url, {
|
||||||
|
profile: { account_id: accountId, auto_offline: autoOffline },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
deleteAvatar() {
|
deleteAvatar() {
|
||||||
return axios.delete(endPoints('deleteAvatar').url);
|
return axios.delete(endPoints('deleteAvatar').url);
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,9 @@ const endPoints = {
|
||||||
availabilityUpdate: {
|
availabilityUpdate: {
|
||||||
url: '/api/v1/profile/availability',
|
url: '/api/v1/profile/availability',
|
||||||
},
|
},
|
||||||
|
autoOffline: {
|
||||||
|
url: '/api/v1/profile/auto_offline',
|
||||||
|
},
|
||||||
logout: {
|
logout: {
|
||||||
url: 'auth/sign_out',
|
url: 'auth/sign_out',
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,12 +18,35 @@
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-divider />
|
<woot-dropdown-divider />
|
||||||
|
<woot-dropdown-item class="auto-offline--toggle">
|
||||||
|
<div class="info-wrap">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
|
||||||
|
icon="info"
|
||||||
|
size="14"
|
||||||
|
class="info-icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="auto-offline--text">
|
||||||
|
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<woot-switch
|
||||||
|
size="small"
|
||||||
|
class="auto-offline--switch"
|
||||||
|
:value="currentUserAutoOffline"
|
||||||
|
@input="updateAutoOffline"
|
||||||
|
/>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-divider />
|
||||||
</woot-dropdown-menu>
|
</woot-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
||||||
|
@ -41,7 +64,7 @@ export default {
|
||||||
AvailabilityStatusBadge,
|
AvailabilityStatusBadge,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [clickaway],
|
mixins: [clickaway, alertMixin],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -54,6 +77,7 @@ export default {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||||
currentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
|
currentUserAutoOffline: 'getCurrentUserAutoOffline',
|
||||||
}),
|
}),
|
||||||
availabilityDisplayLabel() {
|
availabilityDisplayLabel() {
|
||||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||||
|
@ -85,21 +109,30 @@ export default {
|
||||||
closeStatusMenu() {
|
closeStatusMenu() {
|
||||||
this.isStatusMenuOpened = false;
|
this.isStatusMenuOpened = false;
|
||||||
},
|
},
|
||||||
|
updateAutoOffline(autoOffline) {
|
||||||
|
this.$store.dispatch('updateAutoOffline', {
|
||||||
|
accountId: this.currentAccountId,
|
||||||
|
autoOffline,
|
||||||
|
});
|
||||||
|
},
|
||||||
changeAvailabilityStatus(availability) {
|
changeAvailabilityStatus(availability) {
|
||||||
const accountId = this.currentAccountId;
|
|
||||||
if (this.isUpdating) {
|
if (this.isUpdating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isUpdating = true;
|
this.isUpdating = true;
|
||||||
this.$store
|
try {
|
||||||
.dispatch('updateAvailability', {
|
this.$store.dispatch('updateAvailability', {
|
||||||
availability: availability,
|
availability,
|
||||||
account_id: accountId,
|
account_id: this.currentAccountId,
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.isUpdating = false;
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(
|
||||||
|
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isUpdating = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -143,4 +176,32 @@ export default {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-offline--toggle {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-smaller) 0 var(--space-smaller) var(--space-small);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.info-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-offline--switch {
|
||||||
|
margin: -1px var(--space-micro) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-offline--text {
|
||||||
|
margin: 0 var(--space-smaller);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--s-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -135,7 +135,7 @@ export default {
|
||||||
.dropdown-pane {
|
.dropdown-pane {
|
||||||
left: var(--space-slab);
|
left: var(--space-slab);
|
||||||
bottom: var(--space-larger);
|
bottom: var(--space-larger);
|
||||||
min-width: 16.8rem;
|
min-width: 22rem;
|
||||||
z-index: var(--z-index-much-higher);
|
z-index: var(--z-index-low);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="toggle-button"
|
class="toggle-button"
|
||||||
:class="{ active: value }"
|
:class="{ active: value, small: size === 'small' }"
|
||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="value.toString()"
|
:aria-checked="value.toString()"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: { type: Boolean, default: false },
|
value: { type: Boolean, default: false },
|
||||||
|
size: { type: String, default: '' },
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
|
@ -45,6 +46,20 @@ export default {
|
||||||
background-color: var(--w-500);
|
background-color: var(--w-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
width: 22px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
height: var(--space-one);
|
||||||
|
width: var(--space-one);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
transform: translate(var(--space-small), var(--space-zero));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
--space-one-point-five: 1.5rem;
|
--space-one-point-five: 1.5rem;
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
|
|
|
@ -99,7 +99,9 @@
|
||||||
},
|
},
|
||||||
"AVAILABILITY": {
|
"AVAILABILITY": {
|
||||||
"LABEL": "Availability",
|
"LABEL": "Availability",
|
||||||
"STATUSES_LIST": ["Online", "Busy", "Offline"]
|
"STATUSES_LIST": ["Online", "Busy", "Offline"],
|
||||||
|
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
||||||
|
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "Your email address",
|
"LABEL": "Your email address",
|
||||||
|
@ -222,6 +224,10 @@
|
||||||
"CATEGORY": "Category",
|
"CATEGORY": "Category",
|
||||||
"CATEGORY_EMPTY_MESSAGE": "No categories found"
|
"CATEGORY_EMPTY_MESSAGE": "No categories found"
|
||||||
},
|
},
|
||||||
|
"SET_AUTO_OFFLINE": {
|
||||||
|
"TEXT": "Mark offline automatically",
|
||||||
|
"INFO_TEXT": "Let the system automatically mark you offline when you aren't using the app or dashboard."
|
||||||
|
},
|
||||||
"DOCS": "Read docs"
|
"DOCS": "Read docs"
|
||||||
},
|
},
|
||||||
"BILLING_SETTINGS": {
|
"BILLING_SETTINGS": {
|
||||||
|
|
|
@ -48,6 +48,14 @@ export const getters = {
|
||||||
return currentAccount.availability;
|
return currentAccount.availability;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCurrentUserAutoOffline($state, $getters) {
|
||||||
|
const { accounts = [] } = $state.currentUser;
|
||||||
|
const [currentAccount = {}] = accounts.filter(
|
||||||
|
account => account.id === $getters.getCurrentAccountId
|
||||||
|
);
|
||||||
|
return currentAccount.auto_offline;
|
||||||
|
},
|
||||||
|
|
||||||
getCurrentAccountId(_, __, rootState) {
|
getCurrentAccountId(_, __, rootState) {
|
||||||
if (rootState.route.params && rootState.route.params.accountId) {
|
if (rootState.route.params && rootState.route.params.accountId) {
|
||||||
return Number(rootState.route.params.accountId);
|
return Number(rootState.route.params.accountId);
|
||||||
|
@ -174,6 +182,15 @@ export const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateAutoOffline: async ({ commit }, { accountId, autoOffline }) => {
|
||||||
|
try {
|
||||||
|
const response = await authAPI.updateAutoOffline(accountId, autoOffline);
|
||||||
|
commit(types.SET_CURRENT_USER, response.data);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setCurrentUserAvailability({ commit, state: $state }, data) {
|
setCurrentUserAvailability({ commit, state: $state }, data) {
|
||||||
if (data[$state.currentUser.id]) {
|
if (data[$state.currentUser.id]) {
|
||||||
commit(types.SET_CURRENT_USER_AVAILABILITY, data[$state.currentUser.id]);
|
commit(types.SET_CURRENT_USER_AVAILABILITY, data[$state.currentUser.id]);
|
||||||
|
|
|
@ -88,6 +88,38 @@ describe('#actions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#updateAutoOffline', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.post.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
name: 'John',
|
||||||
|
accounts: [
|
||||||
|
{
|
||||||
|
account_id: 1,
|
||||||
|
auto_offline: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
headers: { expiry: 581842904 },
|
||||||
|
});
|
||||||
|
await actions.updateAutoOffline(
|
||||||
|
{ commit, dispatch },
|
||||||
|
{ autoOffline: false, accountId: 1 }
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
types.default.SET_CURRENT_USER,
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'John',
|
||||||
|
accounts: [{ account_id: 1, auto_offline: false }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#updateUISettings', () => {
|
describe('#updateUISettings', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
axios.put.mockResolvedValue({
|
axios.put.mockResolvedValue({
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<li class="dropdown-menu--header" :tabindex="null" :aria-disabled="true">
|
<li class="dropdown-menu--header" :tabindex="null" :aria-disabled="true">
|
||||||
<span class="title">{{ title }}</span>
|
<span class="title">{{ title }}</span>
|
||||||
|
<slot />
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|
1
app/views/api/v1/profiles/auto_offline.jbuilder
Normal file
1
app/views/api/v1/profiles/auto_offline.jbuilder
Normal file
|
@ -0,0 +1 @@
|
||||||
|
json.partial! 'api/v1/models/user', formats: [:json], resource: @user
|
|
@ -77,4 +77,3 @@ fullcontact:
|
||||||
settings_form_schema:
|
settings_form_schema:
|
||||||
[{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }]
|
[{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }]
|
||||||
visible_properties: ['api_key']
|
visible_properties: ['api_key']
|
||||||
|
|
||||||
|
|
|
@ -182,6 +182,7 @@ Rails.application.routes.draw do
|
||||||
delete :avatar, on: :collection
|
delete :avatar, on: :collection
|
||||||
member do
|
member do
|
||||||
post :availability
|
post :availability
|
||||||
|
post :auto_offline
|
||||||
put :set_active_account
|
put :set_active_account
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -196,6 +196,30 @@ RSpec.describe 'Profile API', type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/profile/auto_offline' do
|
||||||
|
context 'when it is an unauthenticated user' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post '/api/v1/profile/auto_offline'
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated user' do
|
||||||
|
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||||
|
|
||||||
|
it 'updates the auto offline status' do
|
||||||
|
post '/api/v1/profile/auto_offline',
|
||||||
|
params: { profile: { auto_offline: false, account_id: account.id } },
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['accounts'].first['auto_offline']).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'PUT /api/v1/profile/set_active_account' do
|
describe 'PUT /api/v1/profile/set_active_account' do
|
||||||
context 'when it is an unauthenticated user' do
|
context 'when it is an unauthenticated user' do
|
||||||
it 'returns unauthorized' do
|
it 'returns unauthorized' do
|
||||||
|
|
Loading…
Reference in a new issue