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:
Pranav Raj S 2022-12-16 11:59:27 -08:00 committed by GitHub
parent 82d3398932
commit aaacf9d4d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 188 additions and 14 deletions

View file

@ -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,

View file

@ -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);
}, },

View file

@ -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',
}, },

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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": {

View file

@ -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]);

View file

@ -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({

View file

@ -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>

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/user', formats: [:json], resource: @user

View file

@ -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']

View file

@ -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

View file

@ -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