Skip to content

Commit

Permalink
Feature: Desktop-view - Implement personal settings for out of office.
Browse files Browse the repository at this point in the history
Co-authored-by: Mantas Masalskis <mm@zammad.com>
Co-authored-by: Florian Liebe <fl@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
  • Loading branch information
4 people committed Apr 18, 2024
1 parent 1a3a955 commit e401688
Show file tree
Hide file tree
Showing 75 changed files with 2,059 additions and 811 deletions.
Expand Up @@ -165,7 +165,7 @@ class ProfileOutOfOffice extends App.ControllerSubContent
message = '» ' + App.i18n.translateInline('Error') + ' «'
@notify
type: 'error'
msg: App.i18n.translateContent(message)
msg: message
removeAll: true

App.Config.set('OutOfOffice', { prio: 2800, name: __('Out of Office'), parent: '#profile', target: '#profile/out_of_office', permission: ['user_preferences.out_of_office+ticket.agent'], controller: ProfileOutOfOffice }, 'NavBarProfile')
20 changes: 10 additions & 10 deletions app/controllers/users_controller.rb
Expand Up @@ -712,16 +712,16 @@ def preferences_notifications_reset

def out_of_office
user = User.find(current_user.id)
user.with_lock do
user.assign_attributes(
out_of_office: params[:out_of_office],
out_of_office_start_at: params[:out_of_office_start_at],
out_of_office_end_at: params[:out_of_office_end_at],
out_of_office_replacement_id: params[:out_of_office_replacement_id],
)
user.preferences[:out_of_office_text] = params[:out_of_office_text]
user.save!
end

Service::User::OutOfOffice
.new(user,
enabled: params[:out_of_office],
start_at: params[:out_of_office_start_at],
end_at: params[:out_of_office_end_at],
replacement: User.find_by(id: params[:out_of_office_replacement_id]),
text: params[:out_of_office_text])
.execute

render json: { message: 'ok' }, status: :ok
end

Expand Down
Expand Up @@ -116,7 +116,7 @@ export default {
}"
:aria-label="i18n.t('Clear Search')"
:aria-hidden="!filter?.length ? 'true' : undefined"
name="backspace"
name="backspace2"
size="tiny"
role="button"
:tabindex="!filter?.length ? '-1' : '0'"
Expand Down
Expand Up @@ -20,7 +20,7 @@ describe('testing input for searching', () => {

expect(search).toHaveAttribute('placeholder', 'Search…')

const clearButton = view.getByIconName('backspace')
const clearButton = view.getByIconName('backspace2')

expect(clearButton).toHaveClass('invisible')

Expand Down
@@ -1,21 +1,16 @@
<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
<script setup lang="ts">
import { markRaw, defineAsyncComponent } from 'vue'
import { markRaw } from 'vue'
import { AutocompleteSearchAgentDocument } from '#shared/components/Form/fields/FieldAgent/graphql/queries/autocompleteSearch/agent.api.ts'
import type { ObjectLike } from '#shared/types/utils.ts'
import type { FormFieldContext } from '#shared/components/Form/types/field.ts'
import type { AutoCompleteProps } from '#shared/components/Form/fields/FieldAutocomplete/types.ts'
import type { AutoCompleteAgentOption } from '#shared/components/Form/fields/FieldAgent/types'
import type { SelectValue } from '#shared/components/CommonSelect/types.ts'
import type { User } from '#shared/graphql/types.ts'
import FieldAgentOptionIcon from './FieldAgentOptionIcon.vue'
const FieldAutoCompleteInput = defineAsyncComponent(
() =>
import(
'#desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue'
),
)
import FieldAutoCompleteInput from '../FieldAutoComplete/FieldAutoCompleteInput.vue'
import FieldAgentOptionIcon from './FieldAgentOptionIcon.vue'
interface Props {
context: FormFieldContext<
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { getByTestId, waitFor } from '@testing-library/vue'
import { FormKit } from '@formkit/vue'
import { renderComponent } from '#tests/support/components/index.ts'
import type { AutocompleteSearchUserEntry } from '#shared/graphql/types.ts'
import { getNode } from '@formkit/core'
import { getNode, type FormKitNode } from '@formkit/core'
import { nullableMock, waitForNextTick } from '#tests/support/utils.ts'
import {
mockAutocompleteSearchAgentQuery,
Expand Down Expand Up @@ -106,21 +106,38 @@ describe('Form - Field - Agent - Features', () => {
name: 'agent_id',
value: 123,
belongsToObjectField: 'user',
// Add manually the "initialEntityObject" which is normally coming
// from the root node (for a single field root node === own node).
plugins: [
(node: FormKitNode) => {
node.context!.initialEntityObject = {
user: {
internalId: 123,
fullname: 'John Doe',
},
}
},
],
},
})

const node = getNode('agent')
await waitForNextTick(true)

expect(wrapper.getByRole('listitem')).toHaveTextContent('John Doe')

node!.context!.initialEntityObject = {
// Reset the field with new value and before change the initial entity object.
const node = getNode('agent')!
node.context!.initialEntityObject = {
user: {
internalId: 123,
fullname: 'John Doe',
internalId: 456,
fullname: 'Jane Doe',
},
}
node.reset('456')

await waitForNextTick(true)

expect(wrapper.getByRole('listitem')).toHaveTextContent('John Doe')
expect(wrapper.getByRole('listitem')).toHaveTextContent('Jane Doe')
})
})

Expand Down
Expand Up @@ -21,6 +21,7 @@ import {
watchOnce,
} from '@vueuse/core'
import { useLazyQuery } from '@vue/apollo-composable'
import type { FormKitNode } from '@formkit/core'
import type { NameNode, OperationDefinitionNode, SelectionNode } from 'graphql'
import CommonInputSearch from '#desktop/components/CommonInputSearch/CommonInputSearch.vue'
import CommonSelect from '#desktop/components/CommonSelect/CommonSelect.vue'
Expand Down Expand Up @@ -82,20 +83,47 @@ watch(
// Remember current optionValueLookup in node context.
contextReactive.value.optionValueLookup = optionValueLookup
// Initial options prefill for non-multiple fields (multiple fields needs to be handled in the form updater).
if (
!props.context.multiple &&
hasValue.value &&
props.context.initialOptionBuilder &&
!getSelectedOptionLabel(currentValue.value)
) {
const initialOption = props.context.initialOptionBuilder(
props.context.node.at('$root')?.context?.initialEntityObject as ObjectLike,
currentValue.value,
props.context,
)
// Initial options prefill for non-multiple fields (multiple fields needs to be handled in
// the form updater or via options prop).
let rememberedInitialOptionFromBuilder: AutoCompleteOption | undefined
const initialOptionBuilderHandler = (rootNode: FormKitNode) => {
if (
hasValue.value &&
props.context.initialOptionBuilder &&
!getSelectedOptionLabel(currentValue.value)
) {
const initialOption = props.context.initialOptionBuilder(
rootNode?.context?.initialEntityObject as ObjectLike,
currentValue.value,
props.context,
)
if (initialOption) {
localOptions.value.push(initialOption)
if (rememberedInitialOptionFromBuilder) {
const rememberedOptionValue = rememberedInitialOptionFromBuilder.value
if (initialOption) localOptions.value.push(initialOption)
localOptions.value = localOptions.value.filter(
(option) => option.value !== rememberedOptionValue,
)
}
rememberedInitialOptionFromBuilder = initialOption
}
}
}
if (!props.context.multiple && props.context.initialOptionBuilder) {
const rootNode = props.context.node.at('$root')
if (rootNode) {
initialOptionBuilderHandler(rootNode)
rootNode?.on('reset', ({ origin }) => {
initialOptionBuilderHandler(origin)
})
}
}
const input = ref<HTMLDivElement>()
Expand Down Expand Up @@ -377,6 +405,7 @@ useFormBlock(contextReactive, openSelectDropdown)
class="formkit-disabled:pointer-events-none flex grow items-center gap-2.5 px-2.5 py-2 text-black focus:outline-none dark:text-white"
:aria-labelledby="`label-${context.id}`"
:aria-disabled="context.disabled"
:aria-describedby="context.describedBy"
aria-autocomplete="none"
:data-multiple="context.multiple"
:tabindex="context.disabled ? '-1' : '0'"
Expand Down
Expand Up @@ -50,13 +50,19 @@ const actionRow = computed(() => ({
showPreview: false,
}))
const inputIcon = computed(() => {
if (contextReactive.value.range) return 'calendar-range'
if (timePicker.value) return 'calendar-date-time'
return 'calendar-event'
})
const { theme } = storeToRefs(useAppTheme())
const dark = computed(() => theme.value === 'dark')
</script>

<template>
<div>
<div class="w-full">
<VueDatePicker
v-model="localValue"
:uid="context.id"
Expand All @@ -81,21 +87,45 @@ const dark = computed(() => theme.value === 'dark')
:action-row="actionRow"
:config="config"
:input-class-name="context.classes.input"
:aria-describedby="context.describedBy"
:aria-labels="ariaLabels"
auto-apply
text-input
offset="12"
v-bind="context.attrs"
@blur="context.handlers.blur"
>
<template #input-icon>
<CommonIcon
:name="context.range ? 'calendar-range' : 'calendar-event'"
size="tiny"
decorative
<template
#dp-input="{
value,
onInput,
onEnter,
onTab,
onFocus,
onBlur,
onKeypress,
onPaste,
}"
>
<input
:id="context.id"
:value="value"
:name="context.node.name"
:class="context.classes.input"
:disabled="context.disabled"
:aria-describedby="context.describedBy"
v-bind="context.attrs"
type="text"
@input="onInput"
@keypress.enter="onEnter"
@keypress.tab="onTab"
@keypress="onKeypress"
@paste="onPaste"
@blur="onBlur"
@focus="onFocus"
/>
</template>
<template #input-icon>
<CommonIcon :name="inputIcon" size="tiny" decorative />
</template>
<template #clear-icon="{ clear }">
<CommonIcon
class="me-3"
Expand Down Expand Up @@ -160,10 +190,6 @@ const dark = computed(() => theme.value === 'dark')
--dp-input-background-color: theme(colors.blue.200);
.dp {
&__input:hover {
outline-color: theme(colors.blue.600);
}
&__clear_icon:hover {
color: theme(colors.black);
}
Expand Down Expand Up @@ -218,10 +244,6 @@ const dark = computed(() => theme.value === 'dark')
--dp-input-background-color: theme(colors.gray.700);
.dp {
&__input:hover {
outline-color: theme(colors.blue.900);
}
&__clear_icon:hover {
color: theme(colors.white);
}
Expand Down Expand Up @@ -273,28 +295,8 @@ const dark = computed(() => theme.value === 'dark')
--dp-time-font-size: theme(fontSize.base);
.dp {
&__input {
background-color: var(--dp-input-background-color);
&:hover {
outline-width: 1px;
outline-style: solid;
outline-offset: 1px;
}
&:focus {
outline-width: 1px;
outline-style: solid;
outline-offset: 1px;
outline-color: theme(colors.blue.800);
}
&:where([data-invalid='true'] *) {
outline-width: 1px;
outline-style: solid;
outline-offset: 1px;
outline-color: theme(colors.red.500) !important;
}
&__input_wrap {
display: flex;
}
&__input_icon {
Expand Down Expand Up @@ -332,6 +334,11 @@ const dark = computed(() => theme.value === 'dark')
&__date_hover:hover,
&__inc_dec_button {
background: theme(colors.transparent);
transition: none;
}
&__date_hover.dp__cell_offset:hover {
color: var(--dp-secondary-color);
}
&__menu_inner {
Expand All @@ -340,6 +347,7 @@ const dark = computed(() => theme.value === 'dark')
&__action_row {
padding-top: 0;
margin-top: theme(space[0.5]);
}
&__btn,
Expand All @@ -364,7 +372,7 @@ const dark = computed(() => theme.value === 'dark')
}
&__calendar_row {
gap: theme(gap.1);
gap: theme(gap[1.5]);
}
&__month_year_wrap {
Expand Down

0 comments on commit e401688

Please sign in to comment.