Skip to content

Commit

Permalink
Feature: Desktop view - Implement the personal setting section to lis…
Browse files Browse the repository at this point in the history
…t and delete user devices.

Co-authored-by: Florian Liebe <fl@zammad.com>
Co-authored-by: Tobias Schäfer <ts@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
  • Loading branch information
5 people committed Apr 30, 2024
1 parent af987e3 commit ad4f291
Show file tree
Hide file tree
Showing 44 changed files with 1,706 additions and 63 deletions.
19 changes: 5 additions & 14 deletions app/controllers/user_devices_controller.rb
Expand Up @@ -26,21 +26,12 @@ def index
end

def destroy

# find device
user_device = UserDevice.find_by(user_id: current_user.id, id: params[:id])

# delete device and session's
if user_device
SessionHelper.list.each do |session|
next if !session.data['user_id']
next if !session.data['user_device_id']
next if session.data['user_device_id'] != user_device.id

SessionHelper.destroy(session.id)
end
user_device.destroy
begin
Service::User::Device::Delete.new(user: current_user, device: UserDevice.find_by(user_id: current_user.id, id: params[:id])).execute
rescue Exceptions::UnprocessableEntity
# noop
end

render json: {}, status: :ok
end

Expand Down
Expand Up @@ -10,10 +10,7 @@ import CommonPopover from '#desktop/components/CommonPopover/CommonPopover.vue'
import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
import CommonPopoverMenu from '#desktop/components/CommonPopover/CommonPopoverMenu.vue'
import { usePopover } from '#desktop/components/CommonPopover/usePopover.ts'
import type {
ButtonSize,
ButtonVariant,
} from '#desktop/components/CommonButton/types.ts'
import type { ButtonSize } from '#desktop/components/CommonButton/types.ts'
import type {
MenuItem,
Orientation,
Expand All @@ -25,16 +22,14 @@ interface Props {
actions: MenuItem[]
entity?: ObjectLike
buttonSize?: ButtonSize
buttonVariant?: ButtonVariant
placement?: Placement
orientation?: Orientation
noSingleActionMode?: boolean
}
const props = withDefaults(defineProps<Props>(), {
buttonSize: 'large',
buttonVariant: 'neutral',
placement: 'start',
buttonSize: 'medium',
placement: 'end',
orientation: 'autoVertical',
})
Expand All @@ -54,14 +49,23 @@ const singleActionMode = computed(() => {
return singleMenuItemPresent.value
})
const buttonVariantClass = computed(() => {
if (singleMenuItem.value?.variant === 'secondary') return 'text-blue-800'
if (singleMenuItem.value?.variant === 'danger') return 'text-red-500'
return 'text-stone-200 dark:text-neutral-500'
})
</script>

<template>
<div v-if="filteredMenuItems" class="inline-block">
<div
v-if="filteredMenuItems && filteredMenuItems.length > 0"
class="inline-block"
>
<CommonButton
v-if="singleActionMode"
:class="buttonVariantClass"
:size="buttonSize"
:variant="buttonVariant"
:aria-label="$t(singleMenuItem?.label)"
:icon="singleMenuItem?.icon"
@click="singleMenuItem?.onClick?.(entity as ObjectLike)"
Expand All @@ -73,11 +77,11 @@ const singleActionMode = computed(() => {
:aria-label="$t('Action menu button')"
aria-haspopup="true"
:aria-controls="menuId"
class="text-stone-200 dark:text-neutral-500"
:class="{
'outline outline-1 outline-offset-1 outline-blue-800': popoverIsOpen,
}"
:size="buttonSize"
:variant="buttonVariant"
icon="three-dots-vertical"
@click="toggle"
/>
Expand Down
Expand Up @@ -40,9 +40,14 @@ const onClickItem = (event: MouseEvent, item: MenuItem) => {
}
const getHoverFocusStyles = (variant?: Variant) => {
if (variant === 'secondary') {
return 'focus-within:bg-blue-500 hover:bg-blue-500 hover:focus-within:bg-blue-500 dark:focus-within:bg-blue-950 dark:hover:bg-blue-950 dark:hover:focus-within:bg-blue-950'
}
if (variant === 'danger') {
return 'focus-within:bg-red-50 hover:bg-red-50 hover:focus-within:bg-red-50 dark:focus-within:bg-red-900 dark:hover:bg-red-900 dark:hover:focus-within:bg-red-900'
return 'focus-within:bg-pink-100 hover:bg-pink-100 hover:focus-within:bg-pink-100 dark:focus-within:bg-red-900 dark:hover:bg-red-900 dark:hover:focus-within:bg-red-900'
}
return 'focus-within:bg-blue-800 focus-within:text-white hover:bg-blue-600 hover:focus-within:bg-blue-800 dark:hover:bg-blue-900 dark:hover:focus-within:bg-blue-800'
}
</script>
Expand Down
Expand Up @@ -17,16 +17,14 @@ export interface Props {
const props = defineProps<Props>()
const variantClass = computed(() => {
if (props.variant === 'danger') {
return 'text-red-500'
}
if (props.variant === 'secondary') return 'text-blue-800'
if (props.variant === 'danger') return 'text-red-500'
return 'group-focus-within:text-white group-hover:text-black group-hover:group-focus-within:text-white dark:group-hover:text-white'
})
const iconColor = computed(() => {
if (props.variant === 'danger') {
return 'text-red-500'
}
if (props.variant === 'secondary') return 'text-blue-800'
if (props.variant === 'danger') return 'text-red-500'
return 'text-stone-200 dark:text-neutral-500 group-hover:text-black dark:group-hover:text-white group-focus-within:text-white group-hover:group-focus-within:text-white'
})
</script>
Expand Down
Expand Up @@ -29,7 +29,7 @@ export type Orientation =

export type Placement = 'start' | 'end'

export type Variant = 'danger'
export type Variant = 'secondary' | 'danger'

export interface MenuItem extends ItemProps {
key: string
Expand Down
Expand Up @@ -22,13 +22,22 @@ defineProps<Props>()
:key="header.key"
class="h-10 p-2.5 text-xs font-normal text-stone-200 ltr:text-left rtl:text-right dark:text-neutral-500"
>
<span>{{ $t(header.label, ...(header.labelPlaceholder || [])) }}</span>
<CommonLabel
class="font-normal text-stone-200 dark:text-neutral-500"
size="small"
>{{
$t(header.label, ...(header.labelPlaceholder || []))
}}</CommonLabel
>

<slot :name="`header-suffix-${header.key}`" :item="header" />
</th>
<th
v-if="actions"
class="h-10 p-2.5 text-xs font-normal text-stone-200 dark:text-neutral-500"
>
<span>{{ $t('Actions') }}</span>
<th v-if="actions" class="h-10 w-0 p-2.5 text-center">
<CommonLabel
class="font-normal text-stone-200 dark:text-neutral-500"
size="small"
>{{ $t('Actions') }}</CommonLabel
>
</th>
</thead>
<tbody>
Expand All @@ -39,14 +48,29 @@ defineProps<Props>()
class="h-10 p-2.5 text-sm text-gray-100 first:rounded-s-md last:rounded-e-md dark:text-neutral-400"
:class="{ 'bg-blue-200 dark:bg-gray-700': (index + 1) % 2 }"
>
<span>{{ item[header.key] || '-' }}</span>
<CommonLabel class="text-black dark:text-white">
<template v-if="!item[header.key]">-</template>
<template v-else-if="header.type === 'timestamp'">
<CommonDateTime :date-time="item[header.key] as string" />
</template>
<template v-else>
{{ item[header.key] }}
</template>
</CommonLabel>

<slot :name="`item-suffix-${header.key}`" :item="item" />
</td>
<td
v-if="actions"
class="h-10 p-2.5 text-center first:rounded-s-md last:rounded-e-md"
class="h-10 w-0 px-2.5 first:rounded-s-md last:rounded-e-md"
:class="{ 'bg-blue-200 dark:bg-gray-700': (index + 1) % 2 }"
>
<CommonActionMenu :actions="actions" :entity="item" />
<CommonActionMenu
class="flex items-center justify-center"
:actions="actions"
:entity="item"
button-size="medium"
/>
</td>
</tr>
</tbody>
Expand Down
Expand Up @@ -37,8 +37,10 @@ const tableActions: MenuItem[] = [
},
]

const renderTable = (props: Props) => {
const renderTable = (props: Props, options = {}) => {
return renderComponent(CommonSimpleTable, {
shallow: false,
...options,
props,
})
}
Expand Down Expand Up @@ -72,13 +74,33 @@ describe('CommonSimpleTable.vue', () => {
expect(view.getByLabelText('Action menu button')).toBeInTheDocument()
})

it('displays the additional data with the item suffix slot', async () => {
const view = renderTable(
{
headers: tableHeaders,
items: tableItems,
actions: tableActions,
},
{
slots: {
'item-suffix-role': '<span>Additional Example</span>',
},
},
)

expect(view.getByText('Additional Example')).toBeInTheDocument()
})

it('generates expected DOM', async () => {
// TODO: check if such snappshot test is really the way we want to go.
const view = renderTable({
headers: tableHeaders,
items: tableItems,
actions: tableActions,
})
const view = renderTable(
{
headers: tableHeaders,
items: tableItems,
actions: tableActions,
},
true,
)

expect(view.baseElement.querySelector('table')).toMatchFileSnapshot(
`${__filename}.snapshot.txt`,
Expand Down
Expand Up @@ -6,23 +6,48 @@
<th
class="h-10 p-2.5 text-xs font-normal text-stone-200 ltr:text-left rtl:text-right dark:text-neutral-500"
>
<span>
<span
class="-:gap-1 -:text-gray-100 -:dark:text-neutral-400 inline-flex items-center justify-start text-xs leading-3 font-normal text-stone-200 dark:text-neutral-500"
data-test-id="common-label"
>
<!--v-if-->

User name

<!--v-if-->
</span>


</th>
<th
class="h-10 p-2.5 text-xs font-normal text-stone-200 ltr:text-left rtl:text-right dark:text-neutral-500"
>
<span>
<span
class="-:gap-1 -:text-gray-100 -:dark:text-neutral-400 inline-flex items-center justify-start text-xs leading-3 font-normal text-stone-200 dark:text-neutral-500"
data-test-id="common-label"
>
<!--v-if-->

Rolle

<!--v-if-->
</span>


</th>

<th
class="h-10 p-2.5 text-xs font-normal text-stone-200 dark:text-neutral-500"
class="h-10 w-0 p-2.5 text-center"
>
<span>
<span
class="-:gap-1 -:text-gray-100 -:dark:text-neutral-400 inline-flex items-center justify-start text-xs leading-3 font-normal text-stone-200 dark:text-neutral-500"
data-test-id="common-label"
>
<!--v-if-->

Actions

<!--v-if-->
</span>
</th>
</thead>
Expand All @@ -33,38 +58,60 @@
<td
class="h-10 p-2.5 text-sm text-gray-100 first:rounded-s-md last:rounded-e-md dark:text-neutral-400 bg-blue-200 dark:bg-gray-700"
>
<span>
<span
class="-:gap-1 -:text-gray-100 -:dark:text-neutral-400 inline-flex items-center justify-start text-sm leading-4 text-black dark:text-white"
data-test-id="common-label"
>
<!--v-if-->


Lindsay Walton


<!--v-if-->
</span>


</td>
<td
class="h-10 p-2.5 text-sm text-gray-100 first:rounded-s-md last:rounded-e-md dark:text-neutral-400 bg-blue-200 dark:bg-gray-700"
>
<span>
<span
class="-:gap-1 -:text-gray-100 -:dark:text-neutral-400 inline-flex items-center justify-start text-sm leading-4 text-black dark:text-white"
data-test-id="common-label"
>
<!--v-if-->


Member


<!--v-if-->
</span>


</td>

<td
class="h-10 p-2.5 text-center first:rounded-s-md last:rounded-e-md bg-blue-200 dark:bg-gray-700"
class="h-10 w-0 px-2.5 first:rounded-s-md last:rounded-e-md bg-blue-200 dark:bg-gray-700"
>
<div
class="inline-block"
class="inline-block flex items-center justify-center"
>
<button
aria-controls="popover-1"
aria-haspopup="true"
aria-label="Action menu button"
class="btn h-min min-h-min flex-nowrap gap-x-1 border-0 font-normal shadow-none hover:outline hover:outline-1 hover:outline-offset-1 hover:outline-blue-600 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:hover:outline-blue-900 btn-secondary bg-transparent hover:bg-transparent text-gray-100 dark:text-neutral-400 btn-lg text-base p-1 rounded-lg w-min"
class="btn h-min min-h-min flex-nowrap gap-x-1 border-0 font-normal shadow-none hover:outline hover:outline-1 hover:outline-offset-1 hover:outline-blue-600 focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:hover:outline-blue-900 btn-secondary bg-transparent hover:bg-transparent text-blue-800 btn-md text-sm p-1 rounded-lg w-min text-stone-200 dark:text-neutral-500"
id="1"
type="button"
>
<!--v-if-->
<svg
aria-hidden="true"
class="icon fill-current icon-three-dots-vertical shrink-0"
height="20"
width="20"
height="16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<use
Expand Down
Expand Up @@ -4,6 +4,7 @@ export interface TableHeader {
key: string
label: string
labelPlaceholder?: string[]
type?: 'timestamp'
}

export interface TableItem {
Expand Down
Expand Up @@ -3,7 +3,7 @@
import { axe } from 'vitest-axe'
import { visitView } from '#tests/support/components/visitView.ts'

describe('testing locale a11y view', async () => {
describe('testing appearance a11y view', async () => {
it('has no accessibility violations', async () => {
const view = await visitView('/personal-setting/appearance')
const results = await axe(view.html())
Expand Down

0 comments on commit ad4f291

Please sign in to comment.