From 79a525aa62ffeddfc311e998f540704b3a08e20d Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Date: Fri, 3 Jun 2022 11:12:22 +0530 Subject: [PATCH] feat: Add Bulk actions to conversations (#4647) Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Pranav Raj S --- app/javascript/dashboard/api/bulkActions.js | 9 + .../dashboard/api/specs/bulkAction.spec.js | 9 + .../dashboard/components/ChatList.vue | 92 ++++++- .../widgets/conversation/ConversationBox.vue | 2 +- .../widgets/conversation/ConversationCard.vue | 61 ++++- .../conversationBulkActions/Actions.vue | 135 ++++++++++ .../conversationBulkActions/AgentSelector.vue | 249 ++++++++++++++++++ .../dashboard/i18n/locale/en/bulkActions.json | 17 ++ .../dashboard/i18n/locale/en/index.js | 2 + app/javascript/dashboard/store/index.js | 2 + .../dashboard/store/modules/bulkActions.js | 44 ++++ .../store/modules/inboxAssignableAgents.js | 9 +- .../modules/specs/bulkActions/actions.spec.js | 28 ++ .../modules/specs/bulkActions/fixtures.js | 5 + .../modules/specs/bulkActions/getters.spec.js | 14 + .../specs/bulkActions/mutations.spec.js | 12 + .../inboxAssignableMembers/actions.spec.js | 4 +- .../dashboard/store/mutation-types.js | 3 + .../shared/assets/stylesheets/colors.scss | 2 +- .../shared/assets/stylesheets/shadows.scss | 5 + .../FluentIcon/dashboard-icons.json | 1 + 21 files changed, 693 insertions(+), 12 deletions(-) create mode 100644 app/javascript/dashboard/api/bulkActions.js create mode 100644 app/javascript/dashboard/api/specs/bulkAction.spec.js create mode 100644 app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Actions.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue create mode 100644 app/javascript/dashboard/i18n/locale/en/bulkActions.json create mode 100644 app/javascript/dashboard/store/modules/bulkActions.js create mode 100644 app/javascript/dashboard/store/modules/specs/bulkActions/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/bulkActions/fixtures.js create mode 100644 app/javascript/dashboard/store/modules/specs/bulkActions/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/bulkActions/mutations.spec.js diff --git a/app/javascript/dashboard/api/bulkActions.js b/app/javascript/dashboard/api/bulkActions.js new file mode 100644 index 000000000..a606c56ac --- /dev/null +++ b/app/javascript/dashboard/api/bulkActions.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class BulkActionsAPI extends ApiClient { + constructor() { + super('bulk_actions', { accountScoped: true }); + } +} + +export default new BulkActionsAPI(); diff --git a/app/javascript/dashboard/api/specs/bulkAction.spec.js b/app/javascript/dashboard/api/specs/bulkAction.spec.js new file mode 100644 index 000000000..aec0b1e3e --- /dev/null +++ b/app/javascript/dashboard/api/specs/bulkAction.spec.js @@ -0,0 +1,9 @@ +import bulkActions from '../bulkActions'; +import ApiClient from '../ApiClient'; + +describe('#BulkActionsAPI', () => { + it('creates correct instance', () => { + expect(bulkActions).toBeInstanceOf(ApiClient); + expect(bulkActions).toHaveProperty('create'); + }); +}); diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 2887e4a08..4a90bf7a1 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -96,6 +96,9 @@ :chat="chat" :conversation-type="conversationType" :show-assignee="showAssigneeInConversationCard" + :selected="isConversationSelected(chat.id)" + @select-conversation="selectConversation" + @de-select-conversation="deSelectConversation" />
@@ -134,6 +137,16 @@ @applyFilter="onApplyFilter" /> + +
@@ -152,6 +165,8 @@ import advancedFilterTypes from './widgets/conversation/advancedFilterItems'; import filterQueryGenerator from '../helper/filterQueryGenerator.js'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue'; +import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Actions.vue'; +import alertMixin from 'shared/mixins/alertMixin'; import { hasPressedAltAndJKey, @@ -166,8 +181,9 @@ export default { ChatFilter, ConversationAdvancedFilter, DeleteCustomViews, + ConversationBulkActions, }, - mixins: [timeMixin, conversationMixin, eventListenerMixins], + mixins: [timeMixin, conversationMixin, eventListenerMixins, alertMixin], props: { conversationInbox: { type: [String, Number], @@ -202,6 +218,8 @@ export default { foldersQuery: {}, showAddFoldersModal: false, showDeleteFoldersModal: false, + selectedConversations: [], + selectedInboxes: [], }; }, computed: { @@ -217,6 +235,7 @@ export default { conversationStats: 'conversationStats/getStats', appliedFilters: 'getAppliedConversationFilters', folders: 'customViews/getCustomViews', + inboxes: 'inboxes/getInboxes', }), hasAppliedFilters() { return this.appliedFilters.length !== 0; @@ -343,6 +362,15 @@ export default { } return {}; }, + allConversationsSelected() { + return ( + JSON.stringify(this.selectedConversations) === + JSON.stringify(this.conversationList.map(item => item.id)) + ); + }, + uniqueInboxes() { + return [...new Set(this.selectedInboxes)]; + }, }, watch: { activeTeam() { @@ -376,6 +404,7 @@ export default { if (this.$route.name !== 'home') { this.$router.push({ name: 'home' }); } + this.resetBulkActions(); this.foldersQuery = filterQueryGenerator(payload); this.$store.dispatch('conversationPage/reset'); this.$store.dispatch('emptyAllConversations'); @@ -441,6 +470,7 @@ export default { } }, resetAndFetchData() { + this.resetBulkActions(); this.$store.dispatch('conversationPage/reset'); this.$store.dispatch('emptyAllConversations'); this.$store.dispatch('clearConversationFilters'); @@ -491,6 +521,7 @@ export default { }, updateAssigneeTab(selectedTab) { if (this.activeAssigneeTab !== selectedTab) { + this.resetBulkActions(); bus.$emit('clearSearchInput'); this.activeAssigneeTab = selectedTab; if (!this.currentPage) { @@ -498,6 +529,10 @@ export default { } } }, + resetBulkActions() { + this.selectedConversations = []; + this.selectedInboxes = []; + }, updateStatusType(index) { if (this.activeStatus !== index) { this.activeStatus = index; @@ -520,6 +555,59 @@ export default { this.fetchConversations(); } }, + isConversationSelected(id) { + return this.selectedConversations.includes(id); + }, + selectConversation(conversationId, inboxId) { + this.selectedConversations.push(conversationId); + this.selectedInboxes.push(inboxId); + }, + deSelectConversation(conversationId, inboxId) { + this.selectedConversations = this.selectedConversations.filter( + item => item !== conversationId + ); + this.selectedInboxes = this.selectedInboxes.filter( + item => item !== inboxId + ); + }, + selectAllConversations(check) { + if (check) { + this.selectedConversations = this.conversationList.map(item => item.id); + this.selectedInboxes = this.conversationList.map(item => item.inbox_id); + } else { + this.resetBulkActions(); + } + }, + async onAssignAgent(agent) { + try { + await this.$store.dispatch('bulkActions/process', { + type: 'Conversation', + ids: this.selectedConversations, + fields: { + assignee_id: agent.id, + }, + }); + this.selectedConversations = []; + this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL')); + } catch (err) { + this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED')); + } + }, + async onResolveConversations() { + try { + await this.$store.dispatch('bulkActions/process', { + type: 'Conversation', + ids: this.selectedConversations, + fields: { + status: 'resolved', + }, + }); + this.selectedConversations = []; + this.showAlert(this.$t('BULK_ACTION.RESOLVE_SUCCESFUL')); + } catch (error) { + this.showAlert(this.$t('BULK_ACTION.RESOLVE_FAILED')); + } + }, }, }; @@ -535,7 +623,7 @@ export default { .conversations-list-wrap { flex-shrink: 0; width: 34rem; - + overflow: hidden; @include breakpoint(large up) { width: 36rem; } diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue index 494bd5c82..d2eb8a4bd 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue @@ -99,7 +99,7 @@ export default { watch: { 'currentChat.inbox_id'(inboxId) { if (inboxId) { - this.$store.dispatch('inboxAssignableAgents/fetch', { inboxId }); + this.$store.dispatch('inboxAssignableAgents/fetch', [inboxId]); } }, 'currentChat.id'() { diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 7e6c89bac..ee12b989a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -5,11 +5,24 @@ active: isActiveChat, 'unread-chat': hasUnread, 'has-inbox-name': showInboxName, + 'conversation-selected': selected, }" + @mouseenter="onCardHover" + @mouseleave="onCardLeave" @click="cardClick(chat)" > + @@ -272,6 +305,10 @@ export default { } } +.conversation-selected { + background: var(--color-background-light); +} + .has-inbox-name { &::v-deep .user-thumbnail-box { margin-top: var(--space-normal); @@ -320,4 +357,22 @@ export default { margin-top: var(--space-minus-micro); vertical-align: middle; } +.checkbox-wrapper { + height: 40px; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + margin-top: var(--space-normal); + cursor: pointer; + &:hover { + background-color: var(--w-100); + } + + input[type='checkbox'] { + margin: var(--space-zero); + cursor: pointer; + } +} diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Actions.vue new file mode 100644 index 000000000..5466f559d --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Actions.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue new file mode 100644 index 000000000..1d7fe6ec0 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/AgentSelector.vue @@ -0,0 +1,249 @@ + + + + + diff --git a/app/javascript/dashboard/i18n/locale/en/bulkActions.json b/app/javascript/dashboard/i18n/locale/en/bulkActions.json new file mode 100644 index 000000000..bfd688bef --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/en/bulkActions.json @@ -0,0 +1,17 @@ +{ + "BULK_ACTION": { + "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", + "AGENT_SELECT_LABEL": "Select Agent", + "ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to", + "GO_BACK_LABEL": "Go back", + "ASSIGN_LABEL": "Assign", + "ASSIGN_AGENT_TOOLTIP": "Assign Agent", + "RESOLVE_TOOLTIP": "Resolve", + "ASSIGN_SUCCESFUL": "Conversations assigned successfully", + "ASSIGN_FAILED": "Failed to assign conversations, please try again", + "RESOLVE_SUCCESFUL": "Conversations resolved successfully", + "RESOLVE_FAILED": "Failed to resolve conversations, please try again", + "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", + "AGENT_LIST_LOADING": "Loading Agents" + } +} diff --git a/app/javascript/dashboard/i18n/locale/en/index.js b/app/javascript/dashboard/i18n/locale/en/index.js index 5c1449fab..b21009322 100644 --- a/app/javascript/dashboard/i18n/locale/en/index.js +++ b/app/javascript/dashboard/i18n/locale/en/index.js @@ -21,6 +21,7 @@ import { default as _setNewPassword } from './setNewPassword.json'; import { default as _settings } from './settings.json'; import { default as _signup } from './signup.json'; import { default as _teamsSettings } from './teamsSettings.json'; +import { default as _bulkActions } from './bulkActions.json'; export default { ..._advancedFilters, @@ -46,4 +47,5 @@ export default { ..._settings, ..._signup, ..._teamsSettings, + ..._bulkActions, }; diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js index 5cd1a0112..4957a27cc 100755 --- a/app/javascript/dashboard/store/index.js +++ b/app/javascript/dashboard/store/index.js @@ -6,6 +6,7 @@ import agents from './modules/agents'; import attributes from './modules/attributes'; import auth from './modules/auth'; import automations from './modules/automations'; +import bulkActions from './modules/bulkActions'; import campaigns from './modules/campaigns'; import cannedResponse from './modules/cannedResponse'; import contactConversations from './modules/contactConversations'; @@ -43,6 +44,7 @@ export default new Vuex.Store({ attributes, auth, automations, + bulkActions, campaigns, cannedResponse, contactConversations, diff --git a/app/javascript/dashboard/store/modules/bulkActions.js b/app/javascript/dashboard/store/modules/bulkActions.js new file mode 100644 index 000000000..709f4875a --- /dev/null +++ b/app/javascript/dashboard/store/modules/bulkActions.js @@ -0,0 +1,44 @@ +import types from '../mutation-types'; +import BulkActionsAPI from '../../api/bulkActions'; + +export const state = { + uiFlags: { + isUpdating: false, + }, +}; + +export const getters = { + getUIFlags(_state) { + return _state.uiFlags; + }, +}; + +export const actions = { + process: async function processAction({ commit }, payload) { + commit(types.SET_BULK_ACTIONS_FLAG, { isUpdating: true }); + try { + await BulkActionsAPI.create(payload); + } catch (error) { + throw new Error(error); + } finally { + commit(types.SET_BULK_ACTIONS_FLAG, { isUpdating: false }); + } + }, +}; + +export const mutations = { + [types.SET_BULK_ACTIONS_FLAG](_state, data) { + _state.uiFlags = { + ..._state.uiFlags, + ...data, + }; + }, +}; + +export default { + namespaced: true, + actions, + state, + getters, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/inboxAssignableAgents.js b/app/javascript/dashboard/store/modules/inboxAssignableAgents.js index 1acb6897a..08baeb536 100644 --- a/app/javascript/dashboard/store/modules/inboxAssignableAgents.js +++ b/app/javascript/dashboard/store/modules/inboxAssignableAgents.js @@ -26,13 +26,16 @@ export const getters = { }; export const actions = { - async fetch({ commit }, { inboxId }) { + async fetch({ commit }, inboxIds) { commit(types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }); try { const { data: { payload }, - } = await AssignableAgentsAPI.get([inboxId]); - commit(types.SET_INBOX_ASSIGNABLE_AGENTS, { inboxId, members: payload }); + } = await AssignableAgentsAPI.get(inboxIds); + commit(types.SET_INBOX_ASSIGNABLE_AGENTS, { + inboxId: inboxIds.join(','), + members: payload, + }); } catch (error) { throw new Error(error); } finally { diff --git a/app/javascript/dashboard/store/modules/specs/bulkActions/actions.spec.js b/app/javascript/dashboard/store/modules/specs/bulkActions/actions.spec.js new file mode 100644 index 000000000..5342c46fd --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/bulkActions/actions.spec.js @@ -0,0 +1,28 @@ +import axios from 'axios'; +import { actions } from '../../bulkActions'; +import * as types from '../../../mutation-types'; +import payload from './fixtures'; +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: payload }); + await actions.process({ commit }, payload); + expect(commit.mock.calls).toEqual([ + [types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: true }], + [types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.process({ commit })).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: true }], + [types.default.SET_BULK_ACTIONS_FLAG, { isUpdating: false }], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/bulkActions/fixtures.js b/app/javascript/dashboard/store/modules/specs/bulkActions/fixtures.js new file mode 100644 index 000000000..3b3606e36 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/bulkActions/fixtures.js @@ -0,0 +1,5 @@ +export default { + type: 'Conversation', + ids: [64, 39], + fields: { assignee_id: 6 }, +}; diff --git a/app/javascript/dashboard/store/modules/specs/bulkActions/getters.spec.js b/app/javascript/dashboard/store/modules/specs/bulkActions/getters.spec.js new file mode 100644 index 000000000..2349e2efb --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/bulkActions/getters.spec.js @@ -0,0 +1,14 @@ +import { getters } from '../../bulkActions'; + +describe('#getters', () => { + it('getUIFlags', () => { + const state = { + uiFlags: { + isUpdating: false, + }, + }; + expect(getters.getUIFlags(state)).toEqual({ + isUpdating: false, + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/bulkActions/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/bulkActions/mutations.spec.js new file mode 100644 index 000000000..29278c72e --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/bulkActions/mutations.spec.js @@ -0,0 +1,12 @@ +import types from '../../../mutation-types'; +import { mutations } from '../../bulkActions'; + +describe('#mutations', () => { + describe('#toggleUiFlag', () => { + it('set update flags', () => { + const state = { uiFlags: { isUpdating: false } }; + mutations[types.SET_BULK_ACTIONS_FLAG](state, { isUpdating: true }); + expect(state.uiFlags.isUpdating).toEqual(true); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js b/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js index 480ee657f..39b21b3c0 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/inboxAssignableMembers/actions.spec.js @@ -12,12 +12,12 @@ describe('#actions', () => { axios.get.mockResolvedValue({ data: { payload: agentsData }, }); - await actions.fetch({ commit }, { inboxId: 1 }); + await actions.fetch({ commit }, [1]); expect(commit.mock.calls).toEqual([ [types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: true }], [ types.SET_INBOX_ASSIGNABLE_AGENTS, - { inboxId: 1, members: agentsData }, + { inboxId: '1', members: agentsData }, ], [types.SET_INBOX_ASSIGNABLE_AGENTS_UI_FLAG, { isFetching: false }], ]); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index b78e468a1..e20efbf78 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -211,6 +211,9 @@ export default { ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW', DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW', + // Bulk Actions + SET_BULK_ACTIONS_FLAG: 'SET_BULK_ACTIONS_FLAG', + // Dashboard Apps SET_DASHBOARD_APPS_UI_FLAG: 'SET_DASHBOARD_APPS_UI_FLAG', SET_DASHBOARD_APPS: 'SET_DASHBOARD_APPS', diff --git a/app/javascript/shared/assets/stylesheets/colors.scss b/app/javascript/shared/assets/stylesheets/colors.scss index 3f64ce58b..82a700929 100644 --- a/app/javascript/shared/assets/stylesheets/colors.scss +++ b/app/javascript/shared/assets/stylesheets/colors.scss @@ -7,7 +7,7 @@ --w-75: #D6EBFF; --w-100: #C2E1FF; --w-200: #99CEFF; - --w-300: ##70BAFF; + --w-300: #70BAFF; --w-400: #47A6FF; --w-500: #1F93FF; --w-600: #1976CC; diff --git a/app/javascript/shared/assets/stylesheets/shadows.scss b/app/javascript/shared/assets/stylesheets/shadows.scss index 0e44e1d26..6b11a427b 100644 --- a/app/javascript/shared/assets/stylesheets/shadows.scss +++ b/app/javascript/shared/assets/stylesheets/shadows.scss @@ -8,4 +8,9 @@ 0 4px 6px -2px rgba(0, 0, 0, 0.05); --shadow-larger: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --shadow-dropdown-pane: + 0 0.8rem 1.6rem rgb(50 50 93 / 8%), + 0 0.4rem 1.2rem rgb(0 0 0 / 7%); + --shadow-bulk-action-container: + 6px 3px 22px 9px rgb(181 181 181 / 25%); } diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index cd52e47f0..d235ffdec 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -98,6 +98,7 @@ "people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z", "people-team-outline": "M14.754 10c.966 0 1.75.784 1.75 1.75v4.749a4.501 4.501 0 0 1-9.002 0V11.75c0-.966.783-1.75 1.75-1.75h5.502Zm0 1.5H9.252a.25.25 0 0 0-.25.25v4.749a3.001 3.001 0 0 0 6.002 0V11.75a.25.25 0 0 0-.25-.25ZM3.75 10h3.381a2.738 2.738 0 0 0-.618 1.5H3.75a.25.25 0 0 0-.25.25v3.249a2.501 2.501 0 0 0 3.082 2.433c.085.504.24.985.453 1.432A4.001 4.001 0 0 1 2 14.999V11.75c0-.966.784-1.75 1.75-1.75Zm13.125 0h3.375c.966 0 1.75.784 1.75 1.75V15a4 4 0 0 1-5.03 3.866c.214-.448.369-.929.455-1.433A2.5 2.5 0 0 0 20.5 15v-3.25a.25.25 0 0 0-.25-.25h-2.757a2.738 2.738 0 0 0-.618-1.5ZM12 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm6.5 1a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-13 0a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm6.5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm6.5 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-13 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z", "person-add-outline": "M17.5 12a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11zm-5.477 2a6.47 6.47 0 0 0-.709 1.5H4.253a.749.749 0 0 0-.75.75v.577c0 .535.192 1.053.54 1.46c1.253 1.469 3.22 2.214 5.957 2.214c.597 0 1.157-.035 1.68-.106c.246.495.553.954.912 1.367c-.795.16-1.66.24-2.592.24c-3.146 0-5.532-.906-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.578A2.249 2.249 0 0 1 4.253 14h7.77zm5.477 0l-.09.008a.5.5 0 0 0-.402.402L17 14.5V17h-2.496l-.09.008a.5.5 0 0 0-.402.402l-.008.09l.008.09a.5.5 0 0 0 .402.402l.09.008H17L17 20.5l.008.09a.5.5 0 0 0 .402.402l.09.008l.09-.008a.5.5 0 0 0 .402-.402L18 20.5V18h2.504l.09-.008a.5.5 0 0 0 .402-.402l.008-.09l-.008-.09a.5.5 0 0 0-.402-.402l-.09-.008H18L18 14.5l-.008-.09a.5.5 0 0 0-.402-.402L17.5 14zM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z", + "person-assign-outline": "M11.313 15.5a6.471 6.471 0 0 1 .709-1.5h-7.77a2.249 2.249 0 0 0-2.249 2.25v.577c0 .892.319 1.756.899 2.435c1.566 1.834 3.952 2.74 7.098 2.74c.931 0 1.796-.08 2.592-.24a6.51 6.51 0 0 1-.913-1.366c-.524.07-1.083.105-1.68.105c-2.737 0-4.703-.745-5.957-2.213a2.25 2.25 0 0 1-.539-1.461v-.578a.75.75 0 0 1 .75-.749h7.06ZM10 2.005a5 5 0 1 1 0 10a5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7ZM23 17.5a5.5 5.5 0 1 1-11 0a5.5 5.5 0 0 1 11 0Zm-4.647-2.853a.5.5 0 0 0-.707.707L19.293 17H15a.5.5 0 1 0 0 1h4.293l-1.647 1.647a.5.5 0 0 0 .707.707l2.5-2.5a.497.497 0 0 0 .147-.345V17.5a.498.498 0 0 0-.15-.357l-2.497-2.496Z", "person-outline": "M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438-1.57 1.834-3.957 2.739-7.102 2.739-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501Zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461 1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z", "power-outline": "M8.204 4.82a.75.75 0 0 1 .634 1.36A7.51 7.51 0 0 0 4.5 12.991c0 4.148 3.358 7.51 7.499 7.51s7.499-3.362 7.499-7.51a7.51 7.51 0 0 0-4.323-6.804.75.75 0 1 1 .637-1.358 9.01 9.01 0 0 1 5.186 8.162c0 4.976-4.029 9.01-9 9.01C7.029 22 3 17.966 3 12.99a9.01 9.01 0 0 1 5.204-8.17ZM12 2.496a.75.75 0 0 1 .743.648l.007.102v7.5a.75.75 0 0 1-1.493.102l-.007-.102v-7.5a.75.75 0 0 1 .75-.75Z", "quote-outline": "M7.5 6a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.555-1.24 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.64-1.737 2.66-3.674 3.077-5.859A2.5 2.5 0 1 1 7.5 6Zm9 0a2.5 2.5 0 0 1 2.495 2.336l.005.206c-.01 3.56-1.238 6.614-3.705 9.223a.75.75 0 1 1-1.09-1.03c1.643-1.738 2.662-3.672 3.078-5.859A2.5 2.5 0 1 1 16.5 6Zm-9 1.5a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Zm9 0a1 1 0 1 0 .993 1.117l.007-.124a1 1 0 0 0-1-.993Z",