Skip to content

Commit

Permalink
Feature: Desktop view - Implement overview preferences.
Browse files Browse the repository at this point in the history
Co-authored-by: Benjamin Scharf <bs@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Florian Liebe <fl@zammad.com>
Co-authored-by: Mantas Masalskis <mm@zammad.com>
  • Loading branch information
4 people committed May 16, 2024
1 parent b72e5ce commit e4582cf
Show file tree
Hide file tree
Showing 40 changed files with 1,675 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class Overviews extends App.ControllerSubContent
url: "#{App.Config.get('api_path')}/user_overview_sortings"
processData: true,
success: (data, status, xhr) =>
App.UserOverviewSortingOverview.refresh(data.overviews)
App.UserOverviewSorting.refresh(data.overview_sortings)
App.UserOverviewSortingOverview.refresh(data.overviews, {clear: true})
App.UserOverviewSorting.refresh(data.overview_sortings, {clear: true})
@render(data)
)

Expand Down
27 changes: 22 additions & 5 deletions app/controllers/user/overview_sortings_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

class User::OverviewSortingsController < ApplicationController
include CanPrioritize

prepend_before_action :authenticate_and_authorize!

def index
Expand All @@ -25,10 +23,29 @@ def update
end

def destroy
model_destroy_render(User::OverviewSorting, params)
ActiveRecord::Base.transaction do
model_destroy_render(User::OverviewSorting, params)
end

Gql::Subscriptions::User::Current::OverviewOrderingUpdates
.trigger_by(current_user)
end

def prio_find(entry_prio)
klass.find_by(overview_id: entry_prio[0], user: current_user)
def prio
overview_ids = params[:prios].map(&:first)

authorized_overviews = Ticket::Overviews
.all(current_user:)
.where(id: overview_ids)
.sort_by { |elem| overview_ids.index(elem.id) }

Service::User::Overview::UpdateOrder
.new(current_user, authorized_overviews)
.execute

Gql::Subscriptions::User::Current::OverviewOrderingUpdates
.trigger_by(current_user)

render json: { success: true }, status: :ok
end
end
1 change: 1 addition & 0 deletions app/frontend/apps/desktop/initializer/3RD-PARTY-ICONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
- `assets/eye.svg`
- `assets/files.svg`
- `assets/filter.svg`
- `assets/grip-vertical.svg`
- `assets/image.svg`
- `assets/info-circle.svg`
- `assets/key.svg`
Expand Down
12 changes: 12 additions & 0 deletions app/frontend/apps/desktop/initializer/assets/grip-vertical.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

import { axe } from 'vitest-axe'

import { visitView } from '#tests/support/components/visitView.ts'
import { mockPermissions } from '#tests/support/mock-permissions.ts'
import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'

import { convertToGraphQLId } from '#shared/graphql/utils.ts'

import { mockUserCurrentOverviewListQuery } from '../graphql/queries/userCurrentOverviewList.mocks.ts'

const userCurrentOverviewList = [
{
id: convertToGraphQLId('Overview', 1),
name: 'Open Tickets',
},
{
id: convertToGraphQLId('Overview', 2),
name: 'My Tickets',
},
{
id: convertToGraphQLId('Overview', 3),
name: 'All Tickets',
},
]

describe('personal settings for token access', () => {
beforeEach(() => {
mockUserCurrent({
firstname: 'John',
lastname: 'Doe',
})
mockPermissions(['user_preferences.overview_sorting'])
})

it('has no accessibility violations', async () => {
mockUserCurrentOverviewListQuery({ userCurrentOverviewList })

const view = await visitView('/personal-setting/ticket-overviews')

const results = await axe(view.html())
expect(results).toHaveNoViolations()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/

import { getAllByRole } from '@testing-library/vue'
import { visitView } from '#tests/support/components/visitView.ts'
import { mockPermissions } from '#tests/support/mock-permissions.ts'
import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
import { waitForNextTick } from '#tests/support/utils.ts'
import { convertToGraphQLId } from '#shared/graphql/utils.ts'
import { mockUserCurrentOverviewListQuery } from '../graphql/queries/userCurrentOverviewList.mocks.ts'
import { mockUserCurrentOverviewResetOrderMutation } from '../graphql/mutations/userCurrentOverviewResetOrder.mocks.ts'
import { getUserCurrentOverviewOrderingUpdatesSubscriptionHandler } from '../graphql/subscriptions/userCurrentOverviewOrderingUpdates.mocks.ts'

const userCurrentOverviewList = [
{
id: convertToGraphQLId('Overview', 1),
name: 'Open Tickets',
},
{
id: convertToGraphQLId('Overview', 2),
name: 'My Tickets',
},
{
id: convertToGraphQLId('Overview', 3),
name: 'All Tickets',
},
]

const userCurrentOverviewListAferReset = userCurrentOverviewList.reverse()

describe('personal settings for token access', () => {
beforeEach(() => {
mockUserCurrent({
firstname: 'John',
lastname: 'Doe',
})
mockPermissions(['user_preferences.overview_sorting'])
})

it('shows the overviews order by priority', async () => {
mockUserCurrentOverviewListQuery({ userCurrentOverviewList })

const view = await visitView('/personal-setting/ticket-overviews')

const overviewContainer = view.getByLabelText('Order of ticket overviews')

const overviews = getAllByRole(overviewContainer, 'listitem')

userCurrentOverviewList.forEach((overview, index) => {
expect(overviews[index]).toHaveTextContent(overview.name)
})
})

// TODO: Cover the update of overview order when the items are moved around the list.
// We may need to implement a testable mechanism for reordering the list, though, as drag events are not fully
// supported in JSDOM due to missing client-rectangle coordinate mocking.
// One approach could be to add keyboard shortcuts for changing the order, or perhaps even hidden buttons.

it('allows to reset the order of overviews', async () => {
mockUserCurrentOverviewListQuery({ userCurrentOverviewList })

const view = await visitView('/personal-setting/ticket-overviews')

mockUserCurrentOverviewResetOrderMutation({
userCurrentOverviewResetOrder: {
success: true,
overviews: userCurrentOverviewListAferReset,
errors: null,
},
})

const resetButton = view.getByRole('button', {
name: 'Reset Overview Order',
})

expect(resetButton).toBeInTheDocument()

await view.events.click(resetButton)

await waitForNextTick()

expect(
await view.findByRole('dialog', { name: 'Confirmation' }),
).toBeInTheDocument()

await view.events.click(view.getByRole('button', { name: 'Yes' }))

await waitForNextTick()

userCurrentOverviewListAferReset.forEach((overview) => {
expect(view.getByText(overview.name)).toBeInTheDocument()
})
})

it('updates the overviews list when a new overview is added', async () => {
mockUserCurrentOverviewListQuery({ userCurrentOverviewList })

const view = await visitView('/personal-setting/ticket-overviews')

const overviewUpdateSubscription =
getUserCurrentOverviewOrderingUpdatesSubscriptionHandler()

userCurrentOverviewList.forEach((overview) => {
expect(view.getByText(overview.name)).toBeInTheDocument()
})

overviewUpdateSubscription.trigger({
userCurrentOverviewOrderingUpdates: {
overviews: [
...userCurrentOverviewList,
{
id: convertToGraphQLId('Overview', 4),
name: 'New Overview',
},
],
},
})

await waitForNextTick()

expect(view.getByText('New Overview')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->

<script setup lang="ts">
import Draggable from 'vuedraggable'
export interface OverviewItem {
id: string
name: string
}
const localValue = defineModel<OverviewItem[]>('modelValue')
</script>

<template>
<div v-if="localValue" class="rounded-lg bg-blue-200 dark:bg-gray-700">
<!-- :TODO if we add proper a11y support -->
<!-- <span class="hidden" aria-live="assertive" >{{assistiveText}}</span>-->
<span id="drag-and-drop-ticket-overviews" class="sr-only">
{{ $t('Drag and drop to reorder ticket overview list items.') }}
</span>

<div class="flex flex-col p-1">
<Draggable
v-model="localValue"
:animation="100"
draggable=".draggable"
role="list"
ghost-class="invisible"
item-key="id"
>
<template #item="{ element }">
<div
role="listitem"
draggable="true"
aria-describedby="drag-and-drop-ticket-overviews"
class="draggable flex h-9 cursor-grab items-center gap-2.5 p-2.5 active:cursor-grabbing"
>
<CommonIcon
class="fill-stone-200 dark:fill-neutral-500"
name="grip-vertical"
size="tiny"
/>
<CommonLabel class="w-full text-black dark:text-white">
{{ $t(element.name) }}
</CommonLabel>
</div>
</template>
</Draggable>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as Types from '#shared/graphql/types.ts';

import gql from 'graphql-tag';
import { ErrorsFragmentDoc } from '../../../../../../shared/graphql/fragments/errors.api';
import * as VueApolloComposable from '@vue/apollo-composable';
import * as VueCompositionApi from 'vue';
export type ReactiveFunction<TParam> = () => TParam;

export const UserCurrentOverviewResetOrderDocument = gql`
mutation userCurrentOverviewResetOrder {
userCurrentOverviewResetOrder {
success
overviews {
id
name
}
errors {
...errors
}
}
}
${ErrorsFragmentDoc}`;
export function useUserCurrentOverviewResetOrderMutation(options: VueApolloComposable.UseMutationOptions<Types.UserCurrentOverviewResetOrderMutation, Types.UserCurrentOverviewResetOrderMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<Types.UserCurrentOverviewResetOrderMutation, Types.UserCurrentOverviewResetOrderMutationVariables>> = {}) {
return VueApolloComposable.useMutation<Types.UserCurrentOverviewResetOrderMutation, Types.UserCurrentOverviewResetOrderMutationVariables>(UserCurrentOverviewResetOrderDocument, options);
}
export type UserCurrentOverviewResetOrderMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<Types.UserCurrentOverviewResetOrderMutation, Types.UserCurrentOverviewResetOrderMutationVariables>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
mutation userCurrentOverviewResetOrder {
userCurrentOverviewResetOrder {
success
overviews {
id
name
}
errors {
...errors
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Types from '#shared/graphql/types.ts';

import * as Mocks from '#tests/graphql/builders/mocks.ts'
import * as Operations from './userCurrentOverviewResetOrder.api.ts'

export function mockUserCurrentOverviewResetOrderMutation(defaults: Mocks.MockDefaultsValue<Types.UserCurrentOverviewResetOrderMutation, Types.UserCurrentOverviewResetOrderMutationVariables>) {
return Mocks.mockGraphQLResult(Operations.UserCurrentOverviewResetOrderDocument, defaults)
}

export function waitForUserCurrentOverviewResetOrderMutationCalls() {
return Mocks.waitForGraphQLMockCalls<Types.UserCurrentOverviewResetOrderMutation>(Operations.UserCurrentOverviewResetOrderDocument)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Types from '#shared/graphql/types.ts';

import gql from 'graphql-tag';
import { ErrorsFragmentDoc } from '../../../../../../shared/graphql/fragments/errors.api';
import * as VueApolloComposable from '@vue/apollo-composable';
import * as VueCompositionApi from 'vue';
export type ReactiveFunction<TParam> = () => TParam;

export const UserCurrentOverviewUpdateOrderDocument = gql`
mutation userCurrentOverviewUpdateOrder($overviewIds: [ID!]!) {
userCurrentOverviewUpdateOrder(overviewIds: $overviewIds) {
success
errors {
...errors
}
}
}
${ErrorsFragmentDoc}`;
export function useUserCurrentOverviewUpdateOrderMutation(options: VueApolloComposable.UseMutationOptions<Types.UserCurrentOverviewUpdateOrderMutation, Types.UserCurrentOverviewUpdateOrderMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<Types.UserCurrentOverviewUpdateOrderMutation, Types.UserCurrentOverviewUpdateOrderMutationVariables>> = {}) {
return VueApolloComposable.useMutation<Types.UserCurrentOverviewUpdateOrderMutation, Types.UserCurrentOverviewUpdateOrderMutationVariables>(UserCurrentOverviewUpdateOrderDocument, options);
}
export type UserCurrentOverviewUpdateOrderMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<Types.UserCurrentOverviewUpdateOrderMutation, Types.UserCurrentOverviewUpdateOrderMutationVariables>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mutation userCurrentOverviewUpdateOrder($overviewIds: [ID!]!) {
userCurrentOverviewUpdateOrder(overviewIds: $overviewIds) {
success
errors {
...errors
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Types from '#shared/graphql/types.ts';

import * as Mocks from '#tests/graphql/builders/mocks.ts'
import * as Operations from './userCurrentOverviewUpdateOrder.api.ts'

export function mockUserCurrentOverviewUpdateOrderMutation(defaults: Mocks.MockDefaultsValue<Types.UserCurrentOverviewUpdateOrderMutation, Types.UserCurrentOverviewUpdateOrderMutationVariables>) {
return Mocks.mockGraphQLResult(Operations.UserCurrentOverviewUpdateOrderDocument, defaults)
}

export function waitForUserCurrentOverviewUpdateOrderMutationCalls() {
return Mocks.waitForGraphQLMockCalls<Types.UserCurrentOverviewUpdateOrderMutation>(Operations.UserCurrentOverviewUpdateOrderDocument)
}

0 comments on commit e4582cf

Please sign in to comment.