mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	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:
		| @@ -18,6 +18,10 @@ class Api::V1::ProfilesController < Api::BaseController | ||||
|     head :ok | ||||
|   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 | ||||
|     @user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability]) | ||||
|   end | ||||
| @@ -37,6 +41,10 @@ class Api::V1::ProfilesController < Api::BaseController | ||||
|     params.require(:profile).permit(:account_id, :availability) | ||||
|   end | ||||
|  | ||||
|   def auto_offline_params | ||||
|     params.require(:profile).permit(:account_id, :auto_offline) | ||||
|   end | ||||
|  | ||||
|   def profile_params | ||||
|     params.require(:profile).permit( | ||||
|       :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() { | ||||
|     return axios.delete(endPoints('deleteAvatar').url); | ||||
|   }, | ||||
|   | ||||
| @@ -16,6 +16,9 @@ const endPoints = { | ||||
|   availabilityUpdate: { | ||||
|     url: '/api/v1/profile/availability', | ||||
|   }, | ||||
|   autoOffline: { | ||||
|     url: '/api/v1/profile/auto_offline', | ||||
|   }, | ||||
|   logout: { | ||||
|     url: 'auth/sign_out', | ||||
|   }, | ||||
|   | ||||
| @@ -18,12 +18,35 @@ | ||||
|       </woot-button> | ||||
|     </woot-dropdown-item> | ||||
|     <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> | ||||
| </template> | ||||
|  | ||||
| <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'; | ||||
| import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; | ||||
| import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader'; | ||||
| @@ -41,7 +64,7 @@ export default { | ||||
|     AvailabilityStatusBadge, | ||||
|   }, | ||||
|  | ||||
|   mixins: [clickaway], | ||||
|   mixins: [clickaway, alertMixin], | ||||
|  | ||||
|   data() { | ||||
|     return { | ||||
| @@ -54,6 +77,7 @@ export default { | ||||
|     ...mapGetters({ | ||||
|       getCurrentUserAvailability: 'getCurrentUserAvailability', | ||||
|       currentAccountId: 'getCurrentAccountId', | ||||
|       currentUserAutoOffline: 'getCurrentUserAutoOffline', | ||||
|     }), | ||||
|     availabilityDisplayLabel() { | ||||
|       const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex( | ||||
| @@ -85,21 +109,30 @@ export default { | ||||
|     closeStatusMenu() { | ||||
|       this.isStatusMenuOpened = false; | ||||
|     }, | ||||
|     updateAutoOffline(autoOffline) { | ||||
|       this.$store.dispatch('updateAutoOffline', { | ||||
|         accountId: this.currentAccountId, | ||||
|         autoOffline, | ||||
|       }); | ||||
|     }, | ||||
|     changeAvailabilityStatus(availability) { | ||||
|       const accountId = this.currentAccountId; | ||||
|       if (this.isUpdating) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this.isUpdating = true; | ||||
|       this.$store | ||||
|         .dispatch('updateAvailability', { | ||||
|           availability: availability, | ||||
|           account_id: accountId, | ||||
|         }) | ||||
|         .finally(() => { | ||||
|           this.isUpdating = false; | ||||
|       try { | ||||
|         this.$store.dispatch('updateAvailability', { | ||||
|           availability, | ||||
|           account_id: this.currentAccountId, | ||||
|         }); | ||||
|       } 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; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .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> | ||||
|   | ||||
| @@ -135,7 +135,7 @@ export default { | ||||
| .dropdown-pane { | ||||
|   left: var(--space-slab); | ||||
|   bottom: var(--space-larger); | ||||
|   min-width: 16.8rem; | ||||
|   z-index: var(--z-index-much-higher); | ||||
|   min-width: 22rem; | ||||
|   z-index: var(--z-index-low); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <button | ||||
|     type="button" | ||||
|     class="toggle-button" | ||||
|     :class="{ active: value }" | ||||
|     :class="{ active: value, small: size === 'small' }" | ||||
|     role="switch" | ||||
|     :aria-checked="value.toString()" | ||||
|     @click="onClick" | ||||
| @@ -15,6 +15,7 @@ | ||||
| export default { | ||||
|   props: { | ||||
|     value: { type: Boolean, default: false }, | ||||
|     size: { type: String, default: '' }, | ||||
|   }, | ||||
|   methods: { | ||||
|     onClick() { | ||||
| @@ -45,6 +46,20 @@ export default { | ||||
|     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 { | ||||
|     --space-one-point-five: 1.5rem; | ||||
|     background-color: var(--white); | ||||
|   | ||||
| @@ -99,7 +99,9 @@ | ||||
|       }, | ||||
|       "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": { | ||||
|         "LABEL": "Your email address", | ||||
| @@ -222,6 +224,10 @@ | ||||
|       "CATEGORY": "Category", | ||||
|       "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" | ||||
|   }, | ||||
|   "BILLING_SETTINGS": { | ||||
|   | ||||
| @@ -48,6 +48,14 @@ export const getters = { | ||||
|     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) { | ||||
|     if (rootState.route.params && 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) { | ||||
|     if (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', () => { | ||||
|     it('sends correct actions if API is success', async () => { | ||||
|       axios.put.mockResolvedValue({ | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| <template> | ||||
|   <li class="dropdown-menu--header" :tabindex="null" :aria-disabled="true"> | ||||
|     <span class="title">{{ title }}</span> | ||||
|     <slot /> | ||||
|   </li> | ||||
| </template> | ||||
| <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: | ||||
|     [{ 'label': 'API Key', 'type': 'text', 'name': 'api_key',"validation": "required", }] | ||||
|   visible_properties: ['api_key'] | ||||
|  | ||||
|   | ||||
| @@ -182,6 +182,7 @@ Rails.application.routes.draw do | ||||
|         delete :avatar, on: :collection | ||||
|         member do | ||||
|           post :availability | ||||
|           post :auto_offline | ||||
|           put :set_active_account | ||||
|         end | ||||
|       end | ||||
|   | ||||
| @@ -196,6 +196,30 @@ RSpec.describe 'Profile API', type: :request do | ||||
|     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 | ||||
|     context 'when it is an unauthenticated user' do | ||||
|       it 'returns unauthorized' do | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S