Feature: Desktop view - Add recently closed list (instead of current recently viewed implementation).
Co-authored-by: Mantas Masalskis <mm@zammad.com> Co-authored-by: Dusan Vuckovic <dv@zammad.com>
This commit is contained in:
@@ -19,9 +19,9 @@ import SubscriptionHandler from '#shared/server/apollo/handler/SubscriptionHandl
|
||||
import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
|
||||
import CommonSectionCollapse from '#desktop/components/CommonSectionCollapse/CommonSectionCollapse.vue'
|
||||
import QuickSearchResultList from '#desktop/components/Search/QuickSearch/QuickSearchResultList/QuickSearchResultList.vue'
|
||||
import { useUserCurrentRecentViewResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentViewReset.api.ts'
|
||||
import { useUserCurrentRecentViewListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentViewList.api.ts'
|
||||
import { useUserCurrentRecentViewUpdatesSubscription } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentViewUpdates.api.ts'
|
||||
import { useUserCurrentRecentCloseResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentCloseReset.api.ts'
|
||||
import { useUserCurrentRecentCloseListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentCloseList.api.ts'
|
||||
import { useUserCurrentRecentCloseUpdatesSubscription } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentCloseUpdates.api.ts'
|
||||
|
||||
import { searchPluginByName } from '../plugins/index.ts'
|
||||
|
||||
@@ -50,25 +50,25 @@ const { isTouchDevice } = useTouchDevice()
|
||||
const { ADD_RECENT_SEARCH_DEBOUNCE_TIME, recentSearches, addSearch, clearSearches, removeSearch } =
|
||||
useRecentSearches()
|
||||
|
||||
const recentViewListQuery = new QueryHandler(
|
||||
useUserCurrentRecentViewListQuery({
|
||||
const recentCloseListQuery = new QueryHandler(
|
||||
useUserCurrentRecentCloseListQuery({
|
||||
limit: 10,
|
||||
}),
|
||||
)
|
||||
|
||||
const recentViewListQueryResult = recentViewListQuery.result()
|
||||
const recentCloseListQueryResult = recentCloseListQuery.result()
|
||||
|
||||
const recentViewListItems = computed(
|
||||
() => recentViewListQueryResult.value?.userCurrentRecentViewList ?? [],
|
||||
const recentCloseListItems = computed(
|
||||
() => recentCloseListQueryResult.value?.userCurrentRecentCloseList ?? [],
|
||||
)
|
||||
|
||||
const recentViewUpdatesSubscription = new SubscriptionHandler(
|
||||
useUserCurrentRecentViewUpdatesSubscription(),
|
||||
const recentCloseUpdatesSubscription = new SubscriptionHandler(
|
||||
useUserCurrentRecentCloseUpdatesSubscription(),
|
||||
)
|
||||
|
||||
recentViewUpdatesSubscription.onResult(({ data }) => {
|
||||
if (data?.userCurrentRecentViewUpdates) {
|
||||
recentViewListQuery.refetch()
|
||||
recentCloseUpdatesSubscription.onResult(({ data }) => {
|
||||
if (data?.userCurrentRecentCloseUpdates) {
|
||||
recentCloseListQuery.refetch()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -113,24 +113,24 @@ const confirmClearRecentSearches = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const recentViewResetMutation = new MutationHandler(useUserCurrentRecentViewResetMutation())
|
||||
const recentCloseResetMutation = new MutationHandler(useUserCurrentRecentCloseResetMutation())
|
||||
|
||||
const confirmClearRecentViewed = async () => {
|
||||
const confirmClearRecentlyClosed = async () => {
|
||||
const confirmed = await waitForConfirmation(
|
||||
__('Are you sure? Your recently viewed items will get lost.'),
|
||||
__('Are you sure? Your recently closed items will get lost.'),
|
||||
{ fullscreen: true },
|
||||
)
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
recentViewResetMutation.send().then(() => {
|
||||
recentCloseResetMutation.send().then(() => {
|
||||
// Evict the list cache immediately, before the subscription update comes in.
|
||||
getApolloClient().cache.evict({ fieldName: 'userCurrentRecentViewList' })
|
||||
getApolloClient().cache.evict({ fieldName: 'userCurrentRecentCloseList' })
|
||||
|
||||
notify({
|
||||
id: 'recent-viewed-cleared',
|
||||
id: 'recently-closed-cleared',
|
||||
type: NotificationTypes.Success,
|
||||
message: __('Recently viewed items were cleared successfully.'),
|
||||
message: __('Recently closed items were cleared successfully.'),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -146,9 +146,9 @@ const { resetQuickSearchInputField } = useQuickSearchInput()
|
||||
:debounce-time="DEBOUNCE_TIME"
|
||||
/>
|
||||
|
||||
<template v-else-if="recentSearches.length > 0 || recentViewListItems.length > 0">
|
||||
<template v-else-if="recentSearches.length || recentCloseListItems.length">
|
||||
<CommonSectionCollapse
|
||||
v-if="recentSearches.length > 0"
|
||||
v-if="recentSearches.length"
|
||||
id="page-recent-searches"
|
||||
:title="__('Recent searches')"
|
||||
:no-header="collapsed"
|
||||
@@ -199,33 +199,34 @@ const { resetQuickSearchInputField } = useQuickSearchInput()
|
||||
</CommonSectionCollapse>
|
||||
|
||||
<CommonSectionCollapse
|
||||
v-if="recentViewListItems.length > 0"
|
||||
id="page-recently-viewed"
|
||||
:title="__('Recently viewed')"
|
||||
v-if="recentCloseListItems.length"
|
||||
id="page-recently-closed"
|
||||
:title="__('Recently closed')"
|
||||
:no-header="collapsed"
|
||||
no-collapse
|
||||
>
|
||||
<template #default="{ headerId }">
|
||||
<nav :aria-labelledby="headerId">
|
||||
<ul class="m-0 flex flex-col gap-1 p-0">
|
||||
<li v-for="item in recentViewListItems" :key="item.id" class="relative">
|
||||
<li v-for="item in recentCloseListItems" :key="item.id" class="relative">
|
||||
<component
|
||||
:is="searchPluginByName[item.__typename!].quickSearchComponent"
|
||||
:item="item"
|
||||
mode="recently-viewed"
|
||||
mode="recently-closed"
|
||||
@click="resetQuickSearchInputField"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-2 mb-1 flex justify-end">
|
||||
<CommonLink link="#" size="small" @click="confirmClearRecentViewed">
|
||||
{{ $t('Clear recently viewed') }}
|
||||
<CommonLink link="#" size="small" @click="confirmClearRecentlyClosed">
|
||||
{{ $t('Clear recently closed') }}
|
||||
</CommonLink>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
</CommonSectionCollapse>
|
||||
</template>
|
||||
|
||||
<CommonLabel v-else>
|
||||
{{ $t('Start typing e.g. the name of a ticket, an organization or a user.') }}
|
||||
</CommonLabel>
|
||||
|
||||
@@ -7,12 +7,17 @@ import { mockPermissions } from '#tests/support/mock-permissions.ts'
|
||||
import { waitForNextTick } from '#tests/support/utils.ts'
|
||||
|
||||
import { useRecentSearches } from '#shared/composables/useRecentSearches.ts'
|
||||
import { EnumTicketStateColorCode, type Organization } from '#shared/graphql/types.ts'
|
||||
import {
|
||||
EnumTicketStateColorCode,
|
||||
type Organization,
|
||||
type Ticket,
|
||||
type User,
|
||||
} from '#shared/graphql/types.ts'
|
||||
import { convertToGraphQLId } from '#shared/graphql/utils.ts'
|
||||
|
||||
import { mockUserCurrentRecentViewResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentViewReset.mocks.ts'
|
||||
import { mockUserCurrentRecentViewListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentViewList.mocks.ts'
|
||||
import { getUserCurrentRecentViewUpdatesSubscriptionHandler } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentViewUpdates.mocks.ts'
|
||||
import { mockUserCurrentRecentCloseResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentCloseReset.mocks.ts'
|
||||
import { mockUserCurrentRecentCloseListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentCloseList.mocks.ts'
|
||||
import { getUserCurrentRecentCloseUpdatesSubscriptionHandler } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentCloseUpdates.mocks.ts'
|
||||
|
||||
import { mockQuickSearchQuery } from '../../graphql/queries/quickSearch.mocks.ts'
|
||||
import QuickSearch from '../QuickSearch.vue'
|
||||
@@ -43,8 +48,8 @@ describe('QuickSearch', () => {
|
||||
})
|
||||
|
||||
describe('default state', () => {
|
||||
it('shows empty state message when no searches or recently viewed items exist', async () => {
|
||||
mockUserCurrentRecentViewListQuery({ userCurrentRecentViewList: [] })
|
||||
it('shows empty state message when no searches or recently closed items exist', async () => {
|
||||
mockUserCurrentRecentCloseListQuery({ userCurrentRecentCloseList: [] })
|
||||
|
||||
const wrapper = await renderQuickSearch()
|
||||
|
||||
@@ -58,7 +63,7 @@ describe('QuickSearch', () => {
|
||||
|
||||
describe('recent searches', () => {
|
||||
beforeEach(() => {
|
||||
mockUserCurrentRecentViewListQuery({ userCurrentRecentViewList: [] })
|
||||
mockUserCurrentRecentCloseListQuery({ userCurrentRecentCloseList: [] })
|
||||
})
|
||||
|
||||
it('displays recent searches when added', async () => {
|
||||
@@ -115,39 +120,39 @@ describe('QuickSearch', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('recently viewed items', () => {
|
||||
const recentlyViewedItems = [
|
||||
describe('recently closed items', () => {
|
||||
const recentlyClosedItems = [
|
||||
{
|
||||
__typename: 'Ticket',
|
||||
id: convertToGraphQLId('Ticket', 2),
|
||||
title: 'Ticket 1',
|
||||
number: '1',
|
||||
stateColorCode: EnumTicketStateColorCode.Open,
|
||||
},
|
||||
} as Ticket,
|
||||
{
|
||||
__typename: 'User',
|
||||
id: convertToGraphQLId('User', 2),
|
||||
internalId: 2,
|
||||
fullname: 'User 1',
|
||||
},
|
||||
} as User,
|
||||
{
|
||||
__typename: 'Organization',
|
||||
id: convertToGraphQLId('Organization', 2),
|
||||
internalId: 2,
|
||||
name: 'Organization 1',
|
||||
},
|
||||
} as Organization,
|
||||
]
|
||||
|
||||
it('displays recently viewed items', async () => {
|
||||
it('displays recently closed items', async () => {
|
||||
mockPermissions(['ticket.agent'])
|
||||
mockUserCurrentRecentViewListQuery({
|
||||
userCurrentRecentViewList: recentlyViewedItems as Organization[],
|
||||
mockUserCurrentRecentCloseListQuery({
|
||||
userCurrentRecentCloseList: recentlyClosedItems,
|
||||
})
|
||||
|
||||
const wrapper = await renderQuickSearch()
|
||||
|
||||
expect(
|
||||
wrapper.getByRole('heading', { level: 3, name: 'Recently viewed' }),
|
||||
wrapper.getByRole('heading', { level: 3, name: 'Recently closed' }),
|
||||
).toBeInTheDocument()
|
||||
|
||||
expect(wrapper.getByRole('link', { name: 'check-circle-noTicket 1' })).toBeInTheDocument()
|
||||
@@ -157,24 +162,24 @@ describe('QuickSearch', () => {
|
||||
expect(wrapper.getByRole('link', { name: 'Organization 1' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('allows clearing all recently viewed items', async () => {
|
||||
mockUserCurrentRecentViewListQuery({
|
||||
userCurrentRecentViewList: recentlyViewedItems as Organization[],
|
||||
it('allows clearing all recently closed items', async () => {
|
||||
mockUserCurrentRecentCloseListQuery({
|
||||
userCurrentRecentCloseList: recentlyClosedItems,
|
||||
})
|
||||
|
||||
mockUserCurrentRecentViewResetMutation({
|
||||
userCurrentRecentViewReset: { success: true },
|
||||
mockUserCurrentRecentCloseResetMutation({
|
||||
userCurrentRecentCloseReset: { success: true },
|
||||
})
|
||||
|
||||
const wrapper = await renderQuickSearch()
|
||||
|
||||
expect(wrapper.getByRole('link', { name: 'Clear recently viewed' })).toBeInTheDocument()
|
||||
expect(wrapper.getByRole('link', { name: 'Clear recently closed' })).toBeInTheDocument()
|
||||
|
||||
mockUserCurrentRecentViewListQuery({ userCurrentRecentViewList: [] })
|
||||
mockUserCurrentRecentCloseListQuery({ userCurrentRecentCloseList: [] })
|
||||
|
||||
await getUserCurrentRecentViewUpdatesSubscriptionHandler().trigger({
|
||||
userCurrentRecentViewUpdates: {
|
||||
recentViewsUpdated: true,
|
||||
await getUserCurrentRecentCloseUpdatesSubscriptionHandler().trigger({
|
||||
userCurrentRecentCloseUpdates: {
|
||||
recentCloseUpdated: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -184,7 +189,7 @@ describe('QuickSearch', () => {
|
||||
wrapper.getByText('Start typing e.g. the name of a ticket, an organization or a user.'),
|
||||
).toBeInTheDocument()
|
||||
|
||||
expect(wrapper.queryByRole('link', { name: 'Clear recently viewed' })).not.toBeInTheDocument()
|
||||
expect(wrapper.queryByRole('link', { name: 'Clear recently closed' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('display search results when typing', async () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { QuickSearchPluginProps } from '../../types.ts'
|
||||
const props = defineProps<QuickSearchPluginProps>()
|
||||
|
||||
const itemLabel = computed(() => {
|
||||
if (props.mode === 'recently-viewed') {
|
||||
if (props.mode === 'recently-closed') {
|
||||
return (props.item as TicketById).title
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export type SearchPlugin = {
|
||||
|
||||
export interface QuickSearchPluginProps {
|
||||
item: ObjectLike
|
||||
mode: 'recently-viewed' | 'quick-search-results'
|
||||
mode: 'recently-closed' | 'quick-search-results'
|
||||
}
|
||||
|
||||
export interface QuickSearchResultData {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
import * as VueApolloComposable from '@vue/apollo-composable';
|
||||
import * as VueCompositionApi from 'vue';
|
||||
export type ReactiveFunction<TParam> = () => TParam;
|
||||
|
||||
export const UserCurrentRecentCloseResetDocument = gql`
|
||||
mutation userCurrentRecentCloseReset {
|
||||
userCurrentRecentCloseReset {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function useUserCurrentRecentCloseResetMutation(options: VueApolloComposable.UseMutationOptions<Types.UserCurrentRecentCloseResetMutation, Types.UserCurrentRecentCloseResetMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<Types.UserCurrentRecentCloseResetMutation, Types.UserCurrentRecentCloseResetMutationVariables>> = {}) {
|
||||
return VueApolloComposable.useMutation<Types.UserCurrentRecentCloseResetMutation, Types.UserCurrentRecentCloseResetMutationVariables>(UserCurrentRecentCloseResetDocument, options);
|
||||
}
|
||||
export type UserCurrentRecentCloseResetMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<Types.UserCurrentRecentCloseResetMutation, Types.UserCurrentRecentCloseResetMutationVariables>;
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation userCurrentRecentCloseReset {
|
||||
userCurrentRecentCloseReset {
|
||||
success
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import * as Mocks from '#tests/graphql/builders/mocks.ts'
|
||||
import * as Operations from './userCurrentRecentCloseReset.api.ts'
|
||||
import * as ErrorTypes from '#shared/types/error.ts'
|
||||
|
||||
export function mockUserCurrentRecentCloseResetMutation(defaults: Mocks.MockDefaultsValue<Types.UserCurrentRecentCloseResetMutation, Types.UserCurrentRecentCloseResetMutationVariables>) {
|
||||
return Mocks.mockGraphQLResult(Operations.UserCurrentRecentCloseResetDocument, defaults)
|
||||
}
|
||||
|
||||
export function waitForUserCurrentRecentCloseResetMutationCalls() {
|
||||
return Mocks.waitForGraphQLMockCalls<Types.UserCurrentRecentCloseResetMutation>(Operations.UserCurrentRecentCloseResetDocument)
|
||||
}
|
||||
|
||||
export function mockUserCurrentRecentCloseResetMutationError(message: string, extensions: {type: ErrorTypes.GraphQLErrorTypes }) {
|
||||
return Mocks.mockGraphQLResultWithError(Operations.UserCurrentRecentCloseResetDocument, message, extensions);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
import * as VueApolloComposable from '@vue/apollo-composable';
|
||||
import * as VueCompositionApi from 'vue';
|
||||
export type ReactiveFunction<TParam> = () => TParam;
|
||||
|
||||
export const UserCurrentRecentViewResetDocument = gql`
|
||||
mutation userCurrentRecentViewReset {
|
||||
userCurrentRecentViewReset {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function useUserCurrentRecentViewResetMutation(options: VueApolloComposable.UseMutationOptions<Types.UserCurrentRecentViewResetMutation, Types.UserCurrentRecentViewResetMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<Types.UserCurrentRecentViewResetMutation, Types.UserCurrentRecentViewResetMutationVariables>> = {}) {
|
||||
return VueApolloComposable.useMutation<Types.UserCurrentRecentViewResetMutation, Types.UserCurrentRecentViewResetMutationVariables>(UserCurrentRecentViewResetDocument, options);
|
||||
}
|
||||
export type UserCurrentRecentViewResetMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<Types.UserCurrentRecentViewResetMutation, Types.UserCurrentRecentViewResetMutationVariables>;
|
||||
@@ -1,5 +0,0 @@
|
||||
mutation userCurrentRecentViewReset {
|
||||
userCurrentRecentViewReset {
|
||||
success
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import * as Mocks from '#tests/graphql/builders/mocks.ts'
|
||||
import * as Operations from './userCurrentRecentViewReset.api.ts'
|
||||
import * as ErrorTypes from '#shared/types/error.ts'
|
||||
|
||||
export function mockUserCurrentRecentViewResetMutation(defaults: Mocks.MockDefaultsValue<Types.UserCurrentRecentViewResetMutation, Types.UserCurrentRecentViewResetMutationVariables>) {
|
||||
return Mocks.mockGraphQLResult(Operations.UserCurrentRecentViewResetDocument, defaults)
|
||||
}
|
||||
|
||||
export function waitForUserCurrentRecentViewResetMutationCalls() {
|
||||
return Mocks.waitForGraphQLMockCalls<Types.UserCurrentRecentViewResetMutation>(Operations.UserCurrentRecentViewResetDocument)
|
||||
}
|
||||
|
||||
export function mockUserCurrentRecentViewResetMutationError(message: string, extensions: {type: ErrorTypes.GraphQLErrorTypes }) {
|
||||
return Mocks.mockGraphQLResultWithError(Operations.UserCurrentRecentViewResetDocument, message, extensions);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
import * as VueApolloComposable from '@vue/apollo-composable';
|
||||
import * as VueCompositionApi from 'vue';
|
||||
export type ReactiveFunction<TParam> = () => TParam;
|
||||
|
||||
export const UserCurrentRecentCloseListDocument = gql`
|
||||
query userCurrentRecentCloseList($limit: Int) {
|
||||
userCurrentRecentCloseList(limit: $limit) {
|
||||
... on Ticket {
|
||||
id
|
||||
internalId
|
||||
title
|
||||
number
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
priority {
|
||||
id
|
||||
name
|
||||
defaultCreate
|
||||
uiColor
|
||||
}
|
||||
stateColorCode
|
||||
}
|
||||
... on User {
|
||||
id
|
||||
internalId
|
||||
fullname
|
||||
active
|
||||
}
|
||||
... on Organization {
|
||||
id
|
||||
internalId
|
||||
name
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function useUserCurrentRecentCloseListQuery(variables: Types.UserCurrentRecentCloseListQueryVariables | VueCompositionApi.Ref<Types.UserCurrentRecentCloseListQueryVariables> | ReactiveFunction<Types.UserCurrentRecentCloseListQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables>> = {}) {
|
||||
return VueApolloComposable.useQuery<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables>(UserCurrentRecentCloseListDocument, variables, options);
|
||||
}
|
||||
export function useUserCurrentRecentCloseListLazyQuery(variables: Types.UserCurrentRecentCloseListQueryVariables | VueCompositionApi.Ref<Types.UserCurrentRecentCloseListQueryVariables> | ReactiveFunction<Types.UserCurrentRecentCloseListQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables>> = {}) {
|
||||
return VueApolloComposable.useLazyQuery<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables>(UserCurrentRecentCloseListDocument, variables, options);
|
||||
}
|
||||
export type UserCurrentRecentCloseListQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables>;
|
||||
@@ -1,5 +1,5 @@
|
||||
query userCurrentRecentViewList($limit: Int) {
|
||||
userCurrentRecentViewList(limit: $limit) {
|
||||
query userCurrentRecentCloseList($limit: Int) {
|
||||
userCurrentRecentCloseList(limit: $limit) {
|
||||
... on Ticket {
|
||||
id
|
||||
internalId
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import * as Mocks from '#tests/graphql/builders/mocks.ts'
|
||||
import * as Operations from './userCurrentRecentCloseList.api.ts'
|
||||
import * as ErrorTypes from '#shared/types/error.ts'
|
||||
|
||||
export function mockUserCurrentRecentCloseListQuery(defaults: Mocks.MockDefaultsValue<Types.UserCurrentRecentCloseListQuery, Types.UserCurrentRecentCloseListQueryVariables>) {
|
||||
return Mocks.mockGraphQLResult(Operations.UserCurrentRecentCloseListDocument, defaults)
|
||||
}
|
||||
|
||||
export function waitForUserCurrentRecentCloseListQueryCalls() {
|
||||
return Mocks.waitForGraphQLMockCalls<Types.UserCurrentRecentCloseListQuery>(Operations.UserCurrentRecentCloseListDocument)
|
||||
}
|
||||
|
||||
export function mockUserCurrentRecentCloseListQueryError(message: string, extensions: {type: ErrorTypes.GraphQLErrorTypes }) {
|
||||
return Mocks.mockGraphQLResultWithError(Operations.UserCurrentRecentCloseListDocument, message, extensions);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
import * as VueApolloComposable from '@vue/apollo-composable';
|
||||
import * as VueCompositionApi from 'vue';
|
||||
export type ReactiveFunction<TParam> = () => TParam;
|
||||
|
||||
export const UserCurrentRecentViewListDocument = gql`
|
||||
query userCurrentRecentViewList($limit: Int) {
|
||||
userCurrentRecentViewList(limit: $limit) {
|
||||
... on Ticket {
|
||||
id
|
||||
internalId
|
||||
title
|
||||
number
|
||||
state {
|
||||
id
|
||||
name
|
||||
}
|
||||
priority {
|
||||
id
|
||||
name
|
||||
defaultCreate
|
||||
uiColor
|
||||
}
|
||||
stateColorCode
|
||||
}
|
||||
... on User {
|
||||
id
|
||||
internalId
|
||||
fullname
|
||||
active
|
||||
}
|
||||
... on Organization {
|
||||
id
|
||||
internalId
|
||||
name
|
||||
active
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function useUserCurrentRecentViewListQuery(variables: Types.UserCurrentRecentViewListQueryVariables | VueCompositionApi.Ref<Types.UserCurrentRecentViewListQueryVariables> | ReactiveFunction<Types.UserCurrentRecentViewListQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables>> = {}) {
|
||||
return VueApolloComposable.useQuery<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables>(UserCurrentRecentViewListDocument, variables, options);
|
||||
}
|
||||
export function useUserCurrentRecentViewListLazyQuery(variables: Types.UserCurrentRecentViewListQueryVariables | VueCompositionApi.Ref<Types.UserCurrentRecentViewListQueryVariables> | ReactiveFunction<Types.UserCurrentRecentViewListQueryVariables> = {}, options: VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables> | VueCompositionApi.Ref<VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables>> | ReactiveFunction<VueApolloComposable.UseQueryOptions<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables>> = {}) {
|
||||
return VueApolloComposable.useLazyQuery<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables>(UserCurrentRecentViewListDocument, variables, options);
|
||||
}
|
||||
export type UserCurrentRecentViewListQueryCompositionFunctionResult = VueApolloComposable.UseQueryReturn<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables>;
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import * as Mocks from '#tests/graphql/builders/mocks.ts'
|
||||
import * as Operations from './userCurrentRecentViewList.api.ts'
|
||||
import * as ErrorTypes from '#shared/types/error.ts'
|
||||
|
||||
export function mockUserCurrentRecentViewListQuery(defaults: Mocks.MockDefaultsValue<Types.UserCurrentRecentViewListQuery, Types.UserCurrentRecentViewListQueryVariables>) {
|
||||
return Mocks.mockGraphQLResult(Operations.UserCurrentRecentViewListDocument, defaults)
|
||||
}
|
||||
|
||||
export function waitForUserCurrentRecentViewListQueryCalls() {
|
||||
return Mocks.waitForGraphQLMockCalls<Types.UserCurrentRecentViewListQuery>(Operations.UserCurrentRecentViewListDocument)
|
||||
}
|
||||
|
||||
export function mockUserCurrentRecentViewListQueryError(message: string, extensions: {type: ErrorTypes.GraphQLErrorTypes }) {
|
||||
return Mocks.mockGraphQLResultWithError(Operations.UserCurrentRecentViewListDocument, message, extensions);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
import * as VueApolloComposable from '@vue/apollo-composable';
|
||||
import * as VueCompositionApi from 'vue';
|
||||
export type ReactiveFunction<TParam> = () => TParam;
|
||||
|
||||
export const UserCurrentRecentCloseUpdatesDocument = gql`
|
||||
subscription userCurrentRecentCloseUpdates {
|
||||
userCurrentRecentCloseUpdates {
|
||||
recentCloseUpdated
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function useUserCurrentRecentCloseUpdatesSubscription(options: VueApolloComposable.UseSubscriptionOptions<Types.UserCurrentRecentCloseUpdatesSubscription, Types.UserCurrentRecentCloseUpdatesSubscriptionVariables> | VueCompositionApi.Ref<VueApolloComposable.UseSubscriptionOptions<Types.UserCurrentRecentCloseUpdatesSubscription, Types.UserCurrentRecentCloseUpdatesSubscriptionVariables>> | ReactiveFunction<VueApolloComposable.UseSubscriptionOptions<Types.UserCurrentRecentCloseUpdatesSubscription, Types.UserCurrentRecentCloseUpdatesSubscriptionVariables>> = {}) {
|
||||
return VueApolloComposable.useSubscription<Types.UserCurrentRecentCloseUpdatesSubscription, Types.UserCurrentRecentCloseUpdatesSubscriptionVariables>(UserCurrentRecentCloseUpdatesDocument, {}, options);
|
||||
}
|
||||
export type UserCurrentRecentCloseUpdatesSubscriptionCompositionFunctionResult = VueApolloComposable.UseSubscriptionReturn<Types.UserCurrentRecentCloseUpdatesSubscription, Types.UserCurrentRecentCloseUpdatesSubscriptionVariables>;
|
||||
@@ -0,0 +1,5 @@
|
||||
subscription userCurrentRecentCloseUpdates {
|
||||
userCurrentRecentCloseUpdates {
|
||||
recentCloseUpdated
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import * as Mocks from '#tests/graphql/builders/mocks.ts'
|
||||
import * as Operations from './userCurrentRecentViewUpdates.api.ts'
|
||||
import * as Operations from './userCurrentRecentCloseUpdates.api.ts'
|
||||
import * as ErrorTypes from '#shared/types/error.ts'
|
||||
|
||||
export function getUserCurrentRecentViewUpdatesSubscriptionHandler() {
|
||||
return Mocks.getGraphQLSubscriptionHandler<Types.UserCurrentRecentViewUpdatesSubscription>(Operations.UserCurrentRecentViewUpdatesDocument)
|
||||
export function getUserCurrentRecentCloseUpdatesSubscriptionHandler() {
|
||||
return Mocks.getGraphQLSubscriptionHandler<Types.UserCurrentRecentCloseUpdatesSubscription>(Operations.UserCurrentRecentCloseUpdatesDocument)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as Types from '#shared/graphql/types.ts';
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
import * as VueApolloComposable from '@vue/apollo-composable';
|
||||
import * as VueCompositionApi from 'vue';
|
||||
export type ReactiveFunction<TParam> = () => TParam;
|
||||
|
||||
export const UserCurrentRecentViewUpdatesDocument = gql`
|
||||
subscription userCurrentRecentViewUpdates {
|
||||
userCurrentRecentViewUpdates {
|
||||
recentViewsUpdated
|
||||
}
|
||||
}
|
||||
`;
|
||||
export function useUserCurrentRecentViewUpdatesSubscription(options: VueApolloComposable.UseSubscriptionOptions<Types.UserCurrentRecentViewUpdatesSubscription, Types.UserCurrentRecentViewUpdatesSubscriptionVariables> | VueCompositionApi.Ref<VueApolloComposable.UseSubscriptionOptions<Types.UserCurrentRecentViewUpdatesSubscription, Types.UserCurrentRecentViewUpdatesSubscriptionVariables>> | ReactiveFunction<VueApolloComposable.UseSubscriptionOptions<Types.UserCurrentRecentViewUpdatesSubscription, Types.UserCurrentRecentViewUpdatesSubscriptionVariables>> = {}) {
|
||||
return VueApolloComposable.useSubscription<Types.UserCurrentRecentViewUpdatesSubscription, Types.UserCurrentRecentViewUpdatesSubscriptionVariables>(UserCurrentRecentViewUpdatesDocument, {}, options);
|
||||
}
|
||||
export type UserCurrentRecentViewUpdatesSubscriptionCompositionFunctionResult = VueApolloComposable.UseSubscriptionReturn<Types.UserCurrentRecentViewUpdatesSubscription, Types.UserCurrentRecentViewUpdatesSubscriptionVariables>;
|
||||
@@ -1,5 +0,0 @@
|
||||
subscription userCurrentRecentViewUpdates {
|
||||
userCurrentRecentViewUpdates {
|
||||
recentViewsUpdated
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { ConfigUpdatesDocument } from '#shared/graphql/subscriptions/configUpdat
|
||||
import { useApplicationStore } from '#shared/stores/application.ts'
|
||||
import { useAuthenticationStore } from '#shared/stores/authentication.ts'
|
||||
|
||||
import { UserCurrentRecentViewUpdatesDocument } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentViewUpdates.api.ts'
|
||||
import { UserCurrentRecentCloseUpdatesDocument } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentCloseUpdates.api.ts'
|
||||
import { UserCurrentTaskbarItemListUpdatesDocument } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentTaskbarItemListUpdates.api.ts'
|
||||
import { UserCurrentTaskbarItemUpdatesDocument } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentTaskbarItemUpdates.api.ts'
|
||||
|
||||
@@ -40,7 +40,7 @@ beforeEach(() => {
|
||||
mockGraphQLSubscription(UserCurrentTaskbarItemUpdatesDocument)
|
||||
mockGraphQLSubscription(UserCurrentTaskbarItemListUpdatesDocument)
|
||||
mockGraphQLSubscription(OnlineNotificationsCountDocument)
|
||||
mockGraphQLSubscription(UserCurrentRecentViewUpdatesDocument)
|
||||
mockGraphQLSubscription(UserCurrentRecentCloseUpdatesDocument)
|
||||
})
|
||||
|
||||
describe('testing login maintenance mode', () => {
|
||||
|
||||
@@ -1789,8 +1789,8 @@ export type Mutations = {
|
||||
userCurrentOverviewUpdateOrder?: Maybe<UserCurrentOverviewUpdateOrderPayload>;
|
||||
/** Check your password */
|
||||
userCurrentPasswordCheck?: Maybe<UserCurrentPasswordCheckPayload>;
|
||||
/** Reset recent view records of the current user */
|
||||
userCurrentRecentViewReset?: Maybe<UserCurrentRecentViewResetPayload>;
|
||||
/** Reset recently closed items of the current user */
|
||||
userCurrentRecentCloseReset?: Maybe<UserCurrentRecentCloseResetPayload>;
|
||||
/** Remove a linked account of the current user's profile */
|
||||
userCurrentRemoveLinkedAccount?: Maybe<UserCurrentRemoveLinkedAccountPayload>;
|
||||
/** Create a taskbar item for the currently logged-in user. */
|
||||
@@ -3013,8 +3013,8 @@ export type Queries = {
|
||||
userCurrentCalendarSubscriptionList: UserPersonalSettingsCalendarSubscriptionsConfig;
|
||||
/** Fetch available device list of the currently logged-in user */
|
||||
userCurrentDeviceList?: Maybe<Array<UserDevice>>;
|
||||
/** Fetch tickets recently viewed by the current user */
|
||||
userCurrentRecentViewList: Array<RecentView>;
|
||||
/** Fetch recently closed objects for the current user */
|
||||
userCurrentRecentCloseList: Array<RecentClose>;
|
||||
/** Fetch available taskbar item list of the currently logged-in user */
|
||||
userCurrentTaskbarItemList?: Maybe<Array<UserTaskbarItem>>;
|
||||
/** Ticket overviews according to the personal sorting of the current user */
|
||||
@@ -3364,7 +3364,7 @@ export type QueriesUserArgs = {
|
||||
|
||||
|
||||
/** All available queries */
|
||||
export type QueriesUserCurrentRecentViewListArgs = {
|
||||
export type QueriesUserCurrentRecentCloseListArgs = {
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
@@ -3394,8 +3394,8 @@ export type QueriesUserCurrentTwoFactorInitiateMethodConfigurationArgs = {
|
||||
token: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
/** Objects recently viewed */
|
||||
export type RecentView = Organization | Ticket | User;
|
||||
/** Recently closed object */
|
||||
export type RecentClose = Organization | Ticket | User;
|
||||
|
||||
/** Roles */
|
||||
export type Role = {
|
||||
@@ -3539,8 +3539,8 @@ export type Subscriptions = {
|
||||
userCurrentDevicesUpdates: UserCurrentDevicesUpdatesPayload;
|
||||
/** Updates to account overview sorting records */
|
||||
userCurrentOverviewOrderingUpdates: UserCurrentOverviewOrderingUpdatesPayload;
|
||||
/** Updates to the recently viewed items of the current user */
|
||||
userCurrentRecentViewUpdates: UserCurrentRecentViewUpdatesPayload;
|
||||
/** Updates for the recently closed items of the current user */
|
||||
userCurrentRecentCloseUpdates: UserCurrentRecentCloseUpdatesPayload;
|
||||
/** Subscription for taskbar item list priority changes */
|
||||
userCurrentTaskbarItemListUpdates: UserCurrentTaskbarItemListUpdatesPayload;
|
||||
/** Changes to the state of a taskbar item of the currently logged-in user */
|
||||
@@ -5294,20 +5294,20 @@ export type UserCurrentPasswordCheckPayload = {
|
||||
token?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
/** Autogenerated return type of UserCurrentRecentViewReset. */
|
||||
export type UserCurrentRecentViewResetPayload = {
|
||||
__typename?: 'UserCurrentRecentViewResetPayload';
|
||||
/** Autogenerated return type of UserCurrentRecentCloseReset. */
|
||||
export type UserCurrentRecentCloseResetPayload = {
|
||||
__typename?: 'UserCurrentRecentCloseResetPayload';
|
||||
/** Errors encountered during execution of the mutation. */
|
||||
errors?: Maybe<Array<UserError>>;
|
||||
/** Was the reset successful? */
|
||||
success: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
/** Autogenerated return type of UserCurrentRecentViewUpdates. */
|
||||
export type UserCurrentRecentViewUpdatesPayload = {
|
||||
__typename?: 'UserCurrentRecentViewUpdatesPayload';
|
||||
/** The recent view list of the user has changed. */
|
||||
recentViewsUpdated?: Maybe<Scalars['Boolean']['output']>;
|
||||
/** Autogenerated return type of UserCurrentRecentCloseUpdates. */
|
||||
export type UserCurrentRecentCloseUpdatesPayload = {
|
||||
__typename?: 'UserCurrentRecentCloseUpdatesPayload';
|
||||
/** The recently closed list of the user has changed. */
|
||||
recentCloseUpdated?: Maybe<Scalars['Boolean']['output']>;
|
||||
};
|
||||
|
||||
/** Autogenerated return type of UserCurrentRemoveLinkedAccount. */
|
||||
@@ -6099,10 +6099,10 @@ export type UserCurrentPasswordCheckMutationVariables = Exact<{
|
||||
|
||||
export type UserCurrentPasswordCheckMutation = { __typename?: 'Mutations', userCurrentPasswordCheck?: { __typename?: 'UserCurrentPasswordCheckPayload', success?: boolean | null, token?: string | null, errors?: Array<{ __typename?: 'UserError', message: string, messagePlaceholder?: Array<string> | null, field?: string | null, exception?: EnumUserErrorException | null }> | null } | null };
|
||||
|
||||
export type UserCurrentRecentViewResetMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
export type UserCurrentRecentCloseResetMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type UserCurrentRecentViewResetMutation = { __typename?: 'Mutations', userCurrentRecentViewReset?: { __typename?: 'UserCurrentRecentViewResetPayload', success: boolean } | null };
|
||||
export type UserCurrentRecentCloseResetMutation = { __typename?: 'Mutations', userCurrentRecentCloseReset?: { __typename?: 'UserCurrentRecentCloseResetPayload', success: boolean } | null };
|
||||
|
||||
export type UserCurrentTaskbarItemAddMutationVariables = Exact<{
|
||||
input: UserTaskbarItemInput;
|
||||
@@ -6165,12 +6165,12 @@ export type UserCurrentTicketScreenBehaviorMutationVariables = Exact<{
|
||||
|
||||
export type UserCurrentTicketScreenBehaviorMutation = { __typename?: 'Mutations', userCurrentTicketScreenBehavior?: { __typename?: 'UserCurrentTicketScreenBehaviorPayload', success: boolean, errors?: Array<{ __typename?: 'UserError', message: string, messagePlaceholder?: Array<string> | null, field?: string | null, exception?: EnumUserErrorException | null }> | null } | null };
|
||||
|
||||
export type UserCurrentRecentViewListQueryVariables = Exact<{
|
||||
export type UserCurrentRecentCloseListQueryVariables = Exact<{
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type UserCurrentRecentViewListQuery = { __typename?: 'Queries', userCurrentRecentViewList: Array<
|
||||
export type UserCurrentRecentCloseListQuery = { __typename?: 'Queries', userCurrentRecentCloseList: Array<
|
||||
| { __typename?: 'Organization', id: string, internalId: number, name?: string | null, active?: boolean | null }
|
||||
| { __typename?: 'Ticket', id: string, internalId: number, title: string, number: string, stateColorCode: EnumTicketStateColorCode, state: { __typename?: 'TicketState', id: string, name: string }, priority: { __typename?: 'TicketPriority', id: string, name: string, defaultCreate: boolean, uiColor?: string | null } }
|
||||
| { __typename?: 'User', id: string, internalId: number, fullname?: string | null, active?: boolean | null }
|
||||
@@ -6189,10 +6189,10 @@ export type UserCurrentTaskbarItemListQuery = { __typename?: 'Queries', userCurr
|
||||
| { __typename?: 'UserTaskbarItemEntityTicketCreate', uid: string, title: string, createArticleTypeKey?: string | null }
|
||||
| null }> | null };
|
||||
|
||||
export type UserCurrentRecentViewUpdatesSubscriptionVariables = Exact<{ [key: string]: never; }>;
|
||||
export type UserCurrentRecentCloseUpdatesSubscriptionVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type UserCurrentRecentViewUpdatesSubscription = { __typename?: 'Subscriptions', userCurrentRecentViewUpdates: { __typename?: 'UserCurrentRecentViewUpdatesPayload', recentViewsUpdated?: boolean | null } };
|
||||
export type UserCurrentRecentCloseUpdatesSubscription = { __typename?: 'Subscriptions', userCurrentRecentCloseUpdates: { __typename?: 'UserCurrentRecentCloseUpdatesPayload', recentCloseUpdated?: boolean | null } };
|
||||
|
||||
export type UserCurrentTaskbarItemListUpdatesSubscriptionVariables = Exact<{
|
||||
app: EnumTaskbarApp;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import type { UserCurrentRecentCloseListQuery } from '#shared/graphql/types.ts'
|
||||
|
||||
export default (): UserCurrentRecentCloseListQuery => {
|
||||
return {
|
||||
userCurrentRecentCloseList: [],
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
import type { UserCurrentRecentViewListQuery } from '#shared/graphql/types.ts'
|
||||
|
||||
export default (): UserCurrentRecentViewListQuery => {
|
||||
return {
|
||||
userCurrentRecentViewList: [],
|
||||
}
|
||||
}
|
||||
20
app/graphql/gql/mutations/user/current/recent_close/reset.rb
Normal file
20
app/graphql/gql/mutations/user/current/recent_close/reset.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Gql::Mutations
|
||||
class User::Current::RecentClose::Reset < BaseMutation
|
||||
description 'Reset recently closed items of the current user'
|
||||
|
||||
field :success, Boolean, null: false, description: 'Was the reset successful?'
|
||||
|
||||
def resolve
|
||||
context
|
||||
.current_user
|
||||
.recent_closes
|
||||
.delete_all
|
||||
|
||||
Gql::Subscriptions::User::Current::RecentClose::Updates.trigger({}, scope: context.current_user.id)
|
||||
|
||||
{ success: true }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,16 +0,0 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Gql::Mutations
|
||||
class User::Current::RecentView::Reset < BaseMutation
|
||||
description 'Reset recent view records of the current user'
|
||||
|
||||
field :success, Boolean, null: false, description: 'Was the reset successful?'
|
||||
|
||||
def resolve
|
||||
RecentView.where(created_by_id: context.current_user.id).delete_all
|
||||
Gql::Subscriptions::User::Current::RecentView::Updates.trigger({}, scope: context.current_user.id)
|
||||
|
||||
{ success: true }
|
||||
end
|
||||
end
|
||||
end
|
||||
18
app/graphql/gql/queries/user/current/recent_close/list.rb
Normal file
18
app/graphql/gql/queries/user/current/recent_close/list.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Gql::Queries
|
||||
class User::Current::RecentClose::List < BaseQuery
|
||||
|
||||
description 'Fetch recently closed objects for the current user'
|
||||
|
||||
argument :limit, Integer, required: false, description: 'Limit for the amount of returned entries'
|
||||
|
||||
type [Gql::Types::RecentCloseType], null: false
|
||||
|
||||
def resolve(limit: 10)
|
||||
Service::User::ListRecentCloses
|
||||
.new(context.current_user, limit:)
|
||||
.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Gql::Queries
|
||||
class User::Current::RecentView::List < BaseQuery
|
||||
|
||||
description 'Fetch tickets recently viewed by the current user'
|
||||
|
||||
argument :limit, Integer, required: false, description: 'Limit for the amount of entries'
|
||||
|
||||
type [Gql::Types::RecentViewType], null: false
|
||||
|
||||
def resolve(limit: 10)
|
||||
::RecentView.list(context.current_user, limit).map do |recent_view|
|
||||
ObjectLookup.by_id(recent_view.recent_view_object_id).safe_constantize.lookup(id: recent_view.o_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Gql::Subscriptions
|
||||
class User::Current::RecentClose::Updates < BaseSubscription
|
||||
|
||||
description 'Updates for the recently closed items of the current user'
|
||||
|
||||
subscription_scope :current_user_id
|
||||
|
||||
field :recent_close_updated, Boolean, null: true, description: 'The recently closed list of the user has changed.'
|
||||
|
||||
def update
|
||||
{ recent_close_updated: true }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,16 +0,0 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Gql::Subscriptions
|
||||
class User::Current::RecentView::Updates < BaseSubscription
|
||||
|
||||
description 'Updates to the recently viewed items of the current user'
|
||||
|
||||
subscription_scope :current_user_id
|
||||
|
||||
field :recent_views_updated, Boolean, null: true, description: 'The recent view list of the user has changed.'
|
||||
|
||||
def update
|
||||
{ recent_views_updated: true }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,8 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module Gql::Types
|
||||
class RecentViewType < BaseUnion
|
||||
description 'Objects recently viewed'
|
||||
class RecentCloseType < BaseUnion
|
||||
description 'Recently closed object'
|
||||
possible_types Gql::Types::TicketType, Gql::Types::UserType, Gql::Types::OrganizationType
|
||||
end
|
||||
end
|
||||
@@ -12610,12 +12610,12 @@
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "userCurrentRecentViewReset",
|
||||
"description": "Reset recent view records of the current user",
|
||||
"name": "userCurrentRecentCloseReset",
|
||||
"description": "Reset recently closed items of the current user",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "UserCurrentRecentViewResetPayload",
|
||||
"name": "UserCurrentRecentCloseResetPayload",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
@@ -18281,12 +18281,12 @@
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "userCurrentRecentViewList",
|
||||
"description": "Fetch tickets recently viewed by the current user",
|
||||
"name": "userCurrentRecentCloseList",
|
||||
"description": "Fetch recently closed objects for the current user",
|
||||
"args": [
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "Limit for the amount of entries",
|
||||
"description": "Limit for the amount of returned entries",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
@@ -18306,7 +18306,7 @@
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "UNION",
|
||||
"name": "RecentView",
|
||||
"name": "RecentClose",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
@@ -18495,8 +18495,8 @@
|
||||
},
|
||||
{
|
||||
"kind": "UNION",
|
||||
"name": "RecentView",
|
||||
"description": "Objects recently viewed",
|
||||
"name": "RecentClose",
|
||||
"description": "Recently closed object",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
@@ -19708,15 +19708,15 @@
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "userCurrentRecentViewUpdates",
|
||||
"description": "Updates to the recently viewed items of the current user",
|
||||
"name": "userCurrentRecentCloseUpdates",
|
||||
"description": "Updates for the recently closed items of the current user",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "UserCurrentRecentViewUpdatesPayload",
|
||||
"name": "UserCurrentRecentCloseUpdatesPayload",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
@@ -29458,8 +29458,8 @@
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "UserCurrentRecentViewResetPayload",
|
||||
"description": "Autogenerated return type of UserCurrentRecentViewReset.",
|
||||
"name": "UserCurrentRecentCloseResetPayload",
|
||||
"description": "Autogenerated return type of UserCurrentRecentCloseReset.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "errors",
|
||||
@@ -29505,12 +29505,12 @@
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "UserCurrentRecentViewUpdatesPayload",
|
||||
"description": "Autogenerated return type of UserCurrentRecentViewUpdates.",
|
||||
"name": "UserCurrentRecentCloseUpdatesPayload",
|
||||
"description": "Autogenerated return type of UserCurrentRecentCloseUpdates.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "recentViewsUpdated",
|
||||
"description": "The recent view list of the user has changed.",
|
||||
"name": "recentCloseUpdated",
|
||||
"description": "The recently closed list of the user has changed.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
|
||||
13
app/models/concerns/has_recent_closes.rb
Normal file
13
app/models/concerns/has_recent_closes.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module HasRecentCloses
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_destroy_commit :recent_close_destroy
|
||||
end
|
||||
|
||||
def recent_close_destroy
|
||||
RecentClose.destroy_logs(self)
|
||||
end
|
||||
end
|
||||
@@ -12,6 +12,7 @@ class Organization < ApplicationModel
|
||||
include ChecksHtmlSanitized
|
||||
include HasObjectManagerAttributes
|
||||
include HasTaskbars
|
||||
include HasRecentCloses
|
||||
include CanSelector
|
||||
include CanPerformChanges
|
||||
|
||||
|
||||
38
app/models/recent_close.rb
Normal file
38
app/models/recent_close.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class RecentClose < ApplicationModel
|
||||
belongs_to :user, optional: false
|
||||
belongs_to :recently_closed_object, polymorphic: true, optional: false
|
||||
|
||||
validates :user, uniqueness: { scope: %i[recently_closed_object_type recently_closed_object_id] }
|
||||
|
||||
after_commit :trigger_subscriptions
|
||||
|
||||
def self.upsert_closing_time!(user, object)
|
||||
transaction do
|
||||
recent_close = find_or_initialize_by(user:, recently_closed_object: object)
|
||||
recent_close.updated_at = Time.current
|
||||
recent_close.save!
|
||||
|
||||
recent_close
|
||||
end
|
||||
end
|
||||
|
||||
def self.cleanup(diff = 3.months)
|
||||
where(updated_at: ...diff.ago)
|
||||
.delete_all
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def self.destroy_logs(object)
|
||||
where(recently_closed_object: object).each(&:destroy)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trigger_subscriptions
|
||||
Gql::Subscriptions::User::Current::RecentClose::Updates
|
||||
.trigger({}, scope: user_id)
|
||||
end
|
||||
end
|
||||
@@ -76,8 +76,6 @@ class RecentView < ApplicationModel
|
||||
data: {}
|
||||
}
|
||||
)
|
||||
|
||||
Gql::Subscriptions::User::Current::RecentView::Updates.trigger({}, scope: created_by_id)
|
||||
end
|
||||
|
||||
def self.access(object, o_id, user)
|
||||
|
||||
@@ -28,6 +28,7 @@ class Taskbar < ApplicationModel
|
||||
after_update :notify_clients
|
||||
after_destroy :update_preferences_infos, :notify_clients
|
||||
after_commit :update_related_taskbars
|
||||
after_destroy_commit :log_recent_close
|
||||
|
||||
association_attributes_ignored :user
|
||||
|
||||
@@ -285,4 +286,12 @@ class Taskbar < ApplicationModel
|
||||
data,
|
||||
)
|
||||
end
|
||||
|
||||
def log_recent_close
|
||||
object = to_object
|
||||
|
||||
return if !object
|
||||
|
||||
RecentClose.upsert_closing_time!(user, to_object)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,6 +14,7 @@ class Ticket < ApplicationModel
|
||||
include HasLinks
|
||||
include HasObjectManagerAttributes
|
||||
include HasTaskbars
|
||||
include HasRecentCloses
|
||||
include Ticket::CallsStatsTicketReopenLog
|
||||
include Ticket::EnqueuesUserTicketCounterJob
|
||||
include Ticket::ResetsPendingTimeSeconds
|
||||
|
||||
@@ -14,6 +14,7 @@ class User < ApplicationModel
|
||||
include HasObjectManagerAttributes
|
||||
include HasTaskbars
|
||||
include HasTwoFactor
|
||||
include HasRecentCloses
|
||||
include CanSelector
|
||||
include CanPerformChanges
|
||||
include User::Assets
|
||||
@@ -43,6 +44,7 @@ class User < ApplicationModel
|
||||
has_many :owner_tickets, class_name: 'Ticket', foreign_key: :owner_id, inverse_of: :owner
|
||||
has_many :overview_sortings, dependent: :destroy
|
||||
has_many :created_recent_views, class_name: 'RecentView', foreign_key: :created_by_id, dependent: :destroy, inverse_of: :created_by
|
||||
has_many :recent_closes, dependent: :delete_all
|
||||
has_many :data_privacy_tasks, as: :deletable
|
||||
belongs_to :organization, inverse_of: :members, optional: true
|
||||
|
||||
|
||||
88
app/services/service/user/list_recent_closes.rb
Normal file
88
app/services/service/user/list_recent_closes.rb
Normal file
@@ -0,0 +1,88 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class Service::User::ListRecentCloses < Service::Base
|
||||
attr_reader :user, :limit
|
||||
|
||||
def initialize(user, limit: 10)
|
||||
super()
|
||||
|
||||
@user = user
|
||||
@limit = limit
|
||||
end
|
||||
|
||||
def execute
|
||||
objects_in_order
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ordered_recent_closes
|
||||
@ordered_recent_closes ||= user.recent_closes.reorder(updated_at: :desc).limit(limit)
|
||||
end
|
||||
|
||||
def grouped_recent_closes
|
||||
@grouped_recent_closes ||= ordered_recent_closes.group_by(&:recently_closed_object_type)
|
||||
end
|
||||
|
||||
def objects_in_order
|
||||
@objects_in_order ||= ordered_recent_closes
|
||||
.filter_map do |item|
|
||||
case item.recently_closed_object_type
|
||||
when 'Ticket'
|
||||
tickets.find { it.id == item.recently_closed_object_id }
|
||||
when 'Organization'
|
||||
organizations.find { it.id == item.recently_closed_object_id }
|
||||
when 'User'
|
||||
users.find { it.id == item.recently_closed_object_id }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def tickets
|
||||
@tickets ||= fetch_tickets
|
||||
end
|
||||
|
||||
def organizations
|
||||
@organizations ||= fetch_organizations
|
||||
end
|
||||
|
||||
def users
|
||||
@users ||= fetch_users
|
||||
end
|
||||
|
||||
def fetch_tickets
|
||||
return [] if grouped_recent_closes['Ticket'].blank?
|
||||
|
||||
ticket_ids = grouped_recent_closes['Ticket'].map(&:recently_closed_object_id)
|
||||
|
||||
TicketPolicy::ReadScope
|
||||
.new(user)
|
||||
.resolve
|
||||
.where(id: ticket_ids)
|
||||
.to_a
|
||||
end
|
||||
|
||||
def fetch_organizations
|
||||
return [] if grouped_recent_closes['Organization'].blank?
|
||||
|
||||
organization_ids = grouped_recent_closes['Organization'].map(&:recently_closed_object_id)
|
||||
|
||||
OrganizationPolicy::Scope
|
||||
.new(user, Organization.all)
|
||||
.resolve
|
||||
.where(id: organization_ids)
|
||||
.to_a
|
||||
end
|
||||
|
||||
def fetch_users
|
||||
return [] if grouped_recent_closes['User'].blank?
|
||||
|
||||
user_ids = grouped_recent_closes['User'].map(&:recently_closed_object_id)
|
||||
|
||||
UserPolicy::Scope
|
||||
.new(user, User.all)
|
||||
.resolve
|
||||
.where(id: user_ids)
|
||||
.to_a
|
||||
end
|
||||
end
|
||||
@@ -6,17 +6,17 @@
|
||||
"fingerprint": "08dca0d9a45edb498cd57df9241c2a39bd0a7c13f1042bfdb8725cfdfe71f327",
|
||||
"check_name": "UnsafeReflection",
|
||||
"message": "Unsafe reflection method `safe_constantize` called on model attribute",
|
||||
"file": "app/graphql/gql/queries/user/current/recent_view/list.rb",
|
||||
"file": "app/graphql/gql/queries/user/current/recent_close/list.rb",
|
||||
"line": 14,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/remote_code_execution/",
|
||||
"code": "ObjectLookup.by_id(recent_view.recent_view_object_id).safe_constantize",
|
||||
"code": "ObjectLookup.by_id(recent_close.recent_close_object_id).safe_constantize",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "Gql::Queries::User::Current::RecentView::List",
|
||||
"class": "Gql::Queries::User::Current::RecentClose::List",
|
||||
"method": "resolve"
|
||||
},
|
||||
"user_input": "ObjectLookup.by_id(recent_view.recent_view_object_id)",
|
||||
"user_input": "ObjectLookup.by_id(recent_close.recent_close_object_id)",
|
||||
"confidence": "Medium",
|
||||
"cwe_id": [
|
||||
470
|
||||
|
||||
@@ -1023,5 +1023,18 @@ class CreateBase < ActiveRecord::Migration[4.2]
|
||||
|
||||
t.index %i[ai_analytics_run_id user_id], unique: true
|
||||
end
|
||||
|
||||
create_table :recent_closes do |t|
|
||||
t.references :recently_closed_object, polymorphic: true, null: false, type: :integer
|
||||
t.references :user, null: false, foreign_key: true, type: :integer
|
||||
|
||||
t.timestamps limit: 3
|
||||
|
||||
t.index %i[recently_closed_object_type recently_closed_object_id user_id],
|
||||
name: 'index_recent_closed_user_object',
|
||||
unique: true
|
||||
|
||||
t.index :updated_at, order: { updated_at: :desc }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
32
db/migrate/20251106095318_create_recent_closes.rb
Normal file
32
db/migrate/20251106095318_create_recent_closes.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class CreateRecentCloses < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
# return if it's a new setup
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
||||
create_table :recent_closes do |t|
|
||||
t.references :recently_closed_object, polymorphic: true, null: false, type: :integer
|
||||
t.references :user, null: false, foreign_key: true, type: :integer
|
||||
|
||||
t.timestamps limit: 3
|
||||
|
||||
t.index %i[recently_closed_object_type recently_closed_object_id user_id],
|
||||
name: 'index_recent_closed_user_object',
|
||||
unique: true
|
||||
|
||||
t.index :updated_at, order: { updated_at: :desc }
|
||||
end
|
||||
|
||||
Scheduler.create_if_not_exists(
|
||||
name: 'Delete old recently closed entries.',
|
||||
method: 'RecentClose.cleanup',
|
||||
period: 1.day,
|
||||
prio: 2,
|
||||
active: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
last_run: Time.current,
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -66,6 +66,15 @@ Scheduler.create_if_not_exists(
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
Scheduler.create_if_not_exists(
|
||||
name: __('Delete old recently closed entries.'),
|
||||
method: 'RecentClose.cleanup',
|
||||
period: 1.day,
|
||||
prio: 2,
|
||||
active: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
Scheduler.create_or_update(
|
||||
name: __('Delete old online notification entries.'),
|
||||
method: 'OnlineNotification.cleanup',
|
||||
|
||||
@@ -1409,7 +1409,7 @@ msgid "Agent idle timeout"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/role.rb:151
|
||||
#: app/models/user.rb:755
|
||||
#: app/models/user.rb:756
|
||||
msgid "Agent limit exceeded, please check your account settings."
|
||||
msgstr ""
|
||||
|
||||
@@ -1862,7 +1862,7 @@ msgstr ""
|
||||
msgid "Are you sure? The user will have to to reconfigure all two-factor authentication methods."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:83
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:84
|
||||
msgid "Are you sure? This recent search will be lost."
|
||||
msgstr ""
|
||||
|
||||
@@ -1884,12 +1884,12 @@ msgstr ""
|
||||
msgid "Are you sure? Your notifications settings will be reset to default."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:100
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:101
|
||||
msgid "Are you sure? Your recent searches will be lost."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:119
|
||||
msgid "Are you sure? Your recently viewed items will get lost."
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:120
|
||||
msgid "Are you sure? Your recently closed items will get lost."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee:54
|
||||
@@ -1992,7 +1992,7 @@ msgstr ""
|
||||
msgid "At least one filter must be provided."
|
||||
msgstr ""
|
||||
|
||||
#: app/models/user.rb:657
|
||||
#: app/models/user.rb:658
|
||||
msgid "At least one identifier (firstname, lastname, phone, mobile or email) for user is required."
|
||||
msgstr ""
|
||||
|
||||
@@ -2005,7 +2005,7 @@ msgid "At least one object must be selected."
|
||||
msgstr ""
|
||||
|
||||
#: app/models/role.rb:128
|
||||
#: app/models/user.rb:734
|
||||
#: app/models/user.rb:735
|
||||
msgid "At least one user needs to have admin permissions."
|
||||
msgstr ""
|
||||
|
||||
@@ -3115,31 +3115,31 @@ msgstr ""
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:160
|
||||
#: db/seeds/schedulers.rb:169
|
||||
msgid "Clean up 'Cti::Log'."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:169
|
||||
#: db/seeds/schedulers.rb:178
|
||||
msgid "Clean up 'DataPrivacyTask'."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:151
|
||||
#: db/seeds/schedulers.rb:160
|
||||
msgid "Clean up 'HttpLog'."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:106
|
||||
#: db/seeds/schedulers.rb:115
|
||||
msgid "Clean up ActiveJob locks."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:215
|
||||
#: db/seeds/schedulers.rb:224
|
||||
msgid "Clean up cache."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:97
|
||||
#: db/seeds/schedulers.rb:106
|
||||
msgid "Clean up closed sessions."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:115
|
||||
#: db/seeds/schedulers.rb:124
|
||||
msgid "Clean up dead sessions."
|
||||
msgstr ""
|
||||
|
||||
@@ -3147,7 +3147,7 @@ msgstr ""
|
||||
msgid "Clean up expired sessions."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:250
|
||||
#: db/seeds/schedulers.rb:259
|
||||
msgid "Clean up mobile taskbars."
|
||||
msgstr ""
|
||||
|
||||
@@ -3172,12 +3172,12 @@ msgstr ""
|
||||
msgid "Clear filter"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:190
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:194
|
||||
msgid "Clear recent searches"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:218
|
||||
msgid "Clear recently viewed"
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:222
|
||||
msgid "Clear recently closed"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/pages/search/components/SearchEmptyMessage.vue:34
|
||||
@@ -3262,7 +3262,7 @@ msgstr ""
|
||||
msgid "Close In Min"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:88
|
||||
#: db/seeds/schedulers.rb:97
|
||||
msgid "Close chat sessions where participants are offline."
|
||||
msgstr ""
|
||||
|
||||
@@ -5210,7 +5210,7 @@ msgstr ""
|
||||
msgid "Delete immediately"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:178
|
||||
#: db/seeds/schedulers.rb:187
|
||||
msgid "Delete obsolete classic IMAP backup."
|
||||
msgstr ""
|
||||
|
||||
@@ -5222,19 +5222,23 @@ msgstr ""
|
||||
msgid "Delete old activity stream entries."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:70
|
||||
#: db/seeds/schedulers.rb:79
|
||||
msgid "Delete old online notification entries."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:142
|
||||
#: db/seeds/schedulers.rb:70
|
||||
msgid "Delete old recently closed entries."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:151
|
||||
msgid "Delete old stats store entries."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:79
|
||||
#: db/seeds/schedulers.rb:88
|
||||
msgid "Delete old token entries."
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:206
|
||||
#: db/seeds/schedulers.rb:215
|
||||
msgid "Delete old upload cache entries."
|
||||
msgstr ""
|
||||
|
||||
@@ -5270,7 +5274,7 @@ msgstr ""
|
||||
msgid "Delete this link"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:175
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:179
|
||||
msgid "Delete this recent search"
|
||||
msgstr ""
|
||||
|
||||
@@ -6034,7 +6038,7 @@ msgstr ""
|
||||
msgid "Email address"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/user.rb:667
|
||||
#: app/models/user.rb:668
|
||||
msgid "Email address '%{email}' is already used for another user."
|
||||
msgstr ""
|
||||
|
||||
@@ -6642,7 +6646,7 @@ msgstr ""
|
||||
msgid "Execute configured macros"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:187
|
||||
#: db/seeds/schedulers.rb:196
|
||||
msgid "Execute import jobs."
|
||||
msgstr ""
|
||||
|
||||
@@ -7283,7 +7287,7 @@ msgstr ""
|
||||
msgid "Generate recovery codes"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:133
|
||||
#: db/seeds/schedulers.rb:142
|
||||
msgid "Generate user-based stats."
|
||||
msgstr ""
|
||||
|
||||
@@ -7652,7 +7656,7 @@ msgstr ""
|
||||
msgid "HTTP type"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:196
|
||||
#: db/seeds/schedulers.rb:205
|
||||
msgid "Handle data privacy tasks."
|
||||
msgstr ""
|
||||
|
||||
@@ -8551,7 +8555,7 @@ msgstr ""
|
||||
msgid "Invalid client_id received!"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/user.rb:606
|
||||
#: app/models/user.rb:607
|
||||
msgid "Invalid email '%{email}'"
|
||||
msgstr ""
|
||||
|
||||
@@ -10123,7 +10127,7 @@ msgstr ""
|
||||
msgid "More information can be found here."
|
||||
msgstr ""
|
||||
|
||||
#: app/models/user.rb:686
|
||||
#: app/models/user.rb:687
|
||||
msgid "More than 250 secondary organizations are not allowed."
|
||||
msgstr ""
|
||||
|
||||
@@ -11147,7 +11151,7 @@ msgstr ""
|
||||
#: app/assets/javascripts/app/views/profile/notification.jst.eco:2
|
||||
#: app/assets/javascripts/app/views/widget/mention.jst.eco:1
|
||||
#: app/assets/javascripts/app/views/widget/online_notification.jst.eco:3
|
||||
#: app/frontend/apps/desktop/components/layout/LayoutSidebar/LeftSidebar/LeftSidebarHeader/OnlineNotification.vue:155
|
||||
#: app/frontend/apps/desktop/components/layout/LayoutSidebar/LeftSidebar/LeftSidebarHeader/OnlineNotification.vue:146
|
||||
#: app/frontend/apps/desktop/components/layout/LayoutSidebar/LeftSidebar/LeftSidebarHeader/OnlineNotification/NotificationPopover/NotificationHeader.vue:20
|
||||
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSetting/plugins/notifications.ts:6
|
||||
#: app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingNotifications.vue:34
|
||||
@@ -12689,16 +12693,16 @@ msgstr ""
|
||||
msgid "Recent Logs"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:94
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:95
|
||||
msgid "Recent search was removed successfully."
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:111
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:112
|
||||
#: app/frontend/apps/mobile/pages/search/views/SearchOverview.vue:251
|
||||
msgid "Recent searches"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:111
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:112
|
||||
msgid "Recent searches were cleared successfully."
|
||||
msgstr ""
|
||||
|
||||
@@ -12708,13 +12712,16 @@ msgstr ""
|
||||
msgid "Recently Viewed Tickets"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_plugin/navigation.coffee:526
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:129
|
||||
msgid "Recently viewed"
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:133
|
||||
msgid "Recently closed"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:129
|
||||
msgid "Recently viewed items were cleared successfully."
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:133
|
||||
msgid "Recently closed items were cleared successfully."
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_plugin/navigation.coffee:526
|
||||
msgid "Recently viewed"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_channel/sms.coffee:397
|
||||
@@ -13787,11 +13794,11 @@ msgstr ""
|
||||
msgid "Secondary organizations"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/user.rb:674
|
||||
#: app/models/user.rb:675
|
||||
msgid "Secondary organizations are only allowed when the primary organization is given."
|
||||
msgstr ""
|
||||
|
||||
#: app/models/user.rb:680
|
||||
#: app/models/user.rb:681
|
||||
msgid "Secondary organizations cannot include the primary organization."
|
||||
msgstr ""
|
||||
|
||||
@@ -14809,7 +14816,7 @@ msgstr ""
|
||||
msgid "Start new conversation"
|
||||
msgstr ""
|
||||
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:226
|
||||
#: app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue:231
|
||||
msgid "Start typing e.g. the name of a ticket, an organization or a user."
|
||||
msgstr ""
|
||||
|
||||
@@ -15145,7 +15152,7 @@ msgstr ""
|
||||
msgid "Switch to old interface"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:124
|
||||
#: db/seeds/schedulers.rb:133
|
||||
msgid "Sync calendars with iCal feeds."
|
||||
msgstr ""
|
||||
|
||||
@@ -18354,7 +18361,7 @@ msgstr ""
|
||||
msgid "Update as closed"
|
||||
msgstr ""
|
||||
|
||||
#: db/seeds/schedulers.rb:241
|
||||
#: db/seeds/schedulers.rb:250
|
||||
msgid "Update exchange oauth 2 token."
|
||||
msgstr ""
|
||||
|
||||
@@ -20582,7 +20589,7 @@ msgstr ""
|
||||
msgid "is the wrong length (should be 1 character)"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/user.rb:871
|
||||
#: app/models/user.rb:872
|
||||
msgid "is too long"
|
||||
msgstr ""
|
||||
|
||||
|
||||
8
spec/factories/recent_close.rb
Normal file
8
spec/factories/recent_close.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
FactoryBot.define do
|
||||
factory :recent_close do
|
||||
recently_closed_object { association(:ticket) }
|
||||
user { association(:agent) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Gql::Mutations::User::Current::RecentClose::Reset, :aggregate_failures, type: :graphql do
|
||||
let(:group) { create(:group) }
|
||||
let(:ticket) { create(:ticket, group:) }
|
||||
let(:user) { create(:agent, groups: [group]) }
|
||||
let(:recent_close_ticket) { create(:recent_close, user:, recently_closed_object: ticket) }
|
||||
let(:recent_close_user) { create(:recent_close, user:, recently_closed_object: user) }
|
||||
|
||||
let(:mutation) do
|
||||
<<~GQL
|
||||
mutation userCurrentRecentCloseReset {
|
||||
userCurrentRecentCloseReset {
|
||||
success
|
||||
}
|
||||
}
|
||||
GQL
|
||||
end
|
||||
|
||||
def execute_graphql_query
|
||||
gql.execute(mutation)
|
||||
end
|
||||
|
||||
context 'when user is authenticated', authenticated_as: :user do
|
||||
before do
|
||||
recent_close_ticket && recent_close_user
|
||||
allow(Gql::Subscriptions::User::Current::RecentClose::Updates).to receive(:trigger)
|
||||
end
|
||||
|
||||
it 'resets recent view entries' do
|
||||
execute_graphql_query
|
||||
expect(gql.result.data[:success]).to be(true)
|
||||
expect(user.recent_closes).to be_empty
|
||||
end
|
||||
|
||||
it 'triggers the recent view updates subscription only once' do
|
||||
execute_graphql_query
|
||||
expect(Gql::Subscriptions::User::Current::RecentClose::Updates).to have_received(:trigger).exactly(1).with({}, scope: user.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not authenticated' do
|
||||
it 'returns an error' do
|
||||
expect(execute_graphql_query.error_message).to eq('Authentication required')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Gql::Mutations::User::Current::RecentView::Reset, :aggregate_failures, type: :graphql do
|
||||
let(:mutation) do
|
||||
<<~GQL
|
||||
mutation userCurrentRecentViewReset {
|
||||
userCurrentRecentViewReset {
|
||||
success
|
||||
}
|
||||
}
|
||||
GQL
|
||||
end
|
||||
|
||||
let(:group) { create(:group) }
|
||||
let(:ticket) { create(:ticket, group:) }
|
||||
let(:user) { create(:agent, groups: [group]) }
|
||||
|
||||
def execute_graphql_query
|
||||
gql.execute(mutation)
|
||||
end
|
||||
|
||||
context 'when user is authenticated', authenticated_as: :user do
|
||||
before do
|
||||
RecentView.log('Ticket', ticket.id, user)
|
||||
RecentView.log('User', user.id, user)
|
||||
allow(Gql::Subscriptions::User::Current::RecentView::Updates).to receive(:trigger)
|
||||
end
|
||||
|
||||
it 'resets recent view entries' do
|
||||
execute_graphql_query
|
||||
expect(gql.result.data[:success]).to be(true)
|
||||
expect(RecentView.list(user)).to be_empty
|
||||
end
|
||||
|
||||
it 'triggers the recent view updates subscription only once' do
|
||||
execute_graphql_query
|
||||
expect(Gql::Subscriptions::User::Current::RecentView::Updates).to have_received(:trigger).exactly(1).with({}, scope: user.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not authenticated' do
|
||||
it 'returns an error' do
|
||||
expect(execute_graphql_query.error_message).to eq('Authentication required')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,11 +2,22 @@
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Gql::Queries::User::Current::RecentView::List, type: :graphql do
|
||||
RSpec.describe Gql::Queries::User::Current::RecentClose::List, type: :graphql do
|
||||
let(:limit) { nil }
|
||||
let(:variables) { { limit: limit } }
|
||||
let(:group) { create(:group) }
|
||||
let(:customer) { create(:customer) }
|
||||
let(:ticket) { create(:ticket, group:, customer:) }
|
||||
let(:inaccessible_customer_ticket) { create(:ticket, customer:) }
|
||||
let(:user) { create(:agent, groups: [group]) }
|
||||
let(:recent_close_ticket) { create(:recent_close, user:, recently_closed_object: ticket, updated_at: 1.minute.ago) }
|
||||
let(:recent_close_user) { create(:recent_close, user:, recently_closed_object: customer) }
|
||||
let(:recent_close_inaccessible_customer_ticket) { create(:recent_close, user: customer, recently_closed_object: inaccessible_customer_ticket) }
|
||||
|
||||
let(:query) do
|
||||
<<~QUERY
|
||||
query userCurrentRecentViewList($limit: Int) {
|
||||
userCurrentRecentViewList(limit: $limit) {
|
||||
query userCurrentRecentCloseList($limit: Int) {
|
||||
userCurrentRecentCloseList(limit: $limit) {
|
||||
__typename
|
||||
... on Ticket {
|
||||
id
|
||||
@@ -21,23 +32,15 @@ RSpec.describe Gql::Queries::User::Current::RecentView::List, type: :graphql do
|
||||
}
|
||||
QUERY
|
||||
end
|
||||
let(:limit) { nil }
|
||||
let(:variables) { { limit: limit } }
|
||||
let(:group) { create(:group) }
|
||||
let(:customer) { create(:customer) }
|
||||
let(:ticket) { create(:ticket, group:, customer:) }
|
||||
let(:inaccessible_customer_ticket) { create(:ticket, customer:) }
|
||||
let(:user) { create(:agent, groups: [group]) }
|
||||
|
||||
before do
|
||||
RecentView.log('Ticket', ticket.id, user)
|
||||
RecentView.log('User', customer.id, user)
|
||||
RecentView.log('Ticket', inaccessible_customer_ticket.id, user)
|
||||
recent_close_ticket && recent_close_user && recent_close_inaccessible_customer_ticket
|
||||
|
||||
gql.execute(query, variables: variables)
|
||||
end
|
||||
|
||||
context 'with an agent', authenticated_as: :user do
|
||||
it 'returns data' do
|
||||
it 'returns expected data' do
|
||||
expect(gql.result.data).to eq(
|
||||
[
|
||||
{ '__typename' => 'User', 'id' => gql.id(customer) },
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Gql::Subscriptions::User::Current::RecentView::Updates, authenticated_as: :agent, type: :graphql do
|
||||
let(:mock_channel) { build_mock_channel }
|
||||
let(:agent) { create(:agent) }
|
||||
RSpec.describe Gql::Subscriptions::User::Current::RecentClose::Updates, authenticated_as: :agent, type: :graphql do
|
||||
let(:mock_channel) { build_mock_channel }
|
||||
let(:agent) { create(:agent) }
|
||||
|
||||
let(:subscription) do
|
||||
<<~QUERY
|
||||
subscription userCurrentRecentViewUpdates {
|
||||
userCurrentRecentViewUpdates {
|
||||
recentViewsUpdated
|
||||
subscription userCurrentRecentCloseUpdates {
|
||||
userCurrentRecentCloseUpdates {
|
||||
recentCloseUpdated
|
||||
}
|
||||
}
|
||||
QUERY
|
||||
@@ -21,20 +22,19 @@ RSpec.describe Gql::Subscriptions::User::Current::RecentView::Updates, authentic
|
||||
|
||||
context 'when subscribed' do
|
||||
it 'subscribes' do
|
||||
expect(gql.result.data).to eq({ 'recentViewsUpdated' => nil })
|
||||
expect(gql.result.data).to eq({ 'recentCloseUpdated' => nil })
|
||||
end
|
||||
|
||||
it 'receives recent_view updates for the current user' do
|
||||
RecentView.log('User', agent.id, agent)
|
||||
expect(mock_channel.mock_broadcasted_messages.first.dig(:result, 'data', 'userCurrentRecentViewUpdates', 'recentViewsUpdated')).to be(true)
|
||||
create(:recent_close, user: agent, recently_closed_object: agent)
|
||||
expect(mock_channel.mock_broadcasted_messages.first.dig(:result, 'data', 'userCurrentRecentCloseUpdates', 'recentCloseUpdated')).to be(true)
|
||||
end
|
||||
|
||||
context 'when another user creates updates' do
|
||||
let(:other_user) { create(:agent) }
|
||||
|
||||
it 'does not receive recent_view updates for another user' do
|
||||
UserInfo.current_user_id = other_user.id # Required to set created_by_id to this user.
|
||||
RecentView.log('User', agent.id, other_user)
|
||||
create(:recent_close, user: other_user, recently_closed_object: agent)
|
||||
expect(mock_channel.mock_broadcasted_messages).to be_empty
|
||||
end
|
||||
end
|
||||
13
spec/models/concerns/has_recent_closes_examples.rb
Normal file
13
spec/models/concerns/has_recent_closes_examples.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
RSpec.shared_examples 'HasRecentCloses' do
|
||||
subject { create(described_class.name.underscore) }
|
||||
|
||||
describe '#destroy_recent_closes' do
|
||||
it 'destroys recent closes' do
|
||||
recent_close = create(:recent_close, recently_closed_object: subject)
|
||||
subject.destroy
|
||||
expect { recent_close.reload }.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,6 +10,7 @@ require 'models/concerns/has_xss_sanitized_note_examples'
|
||||
require 'models/concerns/has_image_sanitized_note_examples'
|
||||
require 'models/concerns/has_object_manager_attributes_examples'
|
||||
require 'models/concerns/has_taskbars_examples'
|
||||
require 'models/concerns/has_recent_closes_examples'
|
||||
|
||||
RSpec.describe Organization, type: :model do
|
||||
subject(:organization) { create(:organization) }
|
||||
@@ -23,6 +24,7 @@ RSpec.describe Organization, type: :model do
|
||||
it_behaves_like 'HasImageSanitizedNote', model_factory: :organization
|
||||
it_behaves_like 'HasObjectManagerAttributes'
|
||||
it_behaves_like 'HasTaskbars'
|
||||
it_behaves_like 'HasRecentCloses'
|
||||
|
||||
describe 'Class methods:' do
|
||||
describe '.where_or_cis' do
|
||||
|
||||
189
spec/models/recent_close_spec.rb
Normal file
189
spec/models/recent_close_spec.rb
Normal file
@@ -0,0 +1,189 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe RecentClose, type: :model do
|
||||
subject { create(:recent_close) }
|
||||
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:recently_closed_object) }
|
||||
it { is_expected.to validate_uniqueness_of(:user).scoped_to(:recently_closed_object_type, :recently_closed_object_id) }
|
||||
|
||||
describe '.upsert_closing_time!' do
|
||||
let(:user) { create(:agent) }
|
||||
let(:object) { create(:ticket) }
|
||||
|
||||
it 'creates a new recent close record' do
|
||||
recent_close = described_class.upsert_closing_time!(user, object)
|
||||
|
||||
expect(recent_close).to have_attributes(
|
||||
id: be_present,
|
||||
user:,
|
||||
recently_closed_object: object
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates a new recent close if none exists' do
|
||||
expect { described_class.upsert_closing_time!(user, object) }
|
||||
.to change(described_class, :count).by(1)
|
||||
end
|
||||
|
||||
it 'updates timestamp if a recent close already exists' do
|
||||
existing = create(:recent_close, user:, recently_closed_object: object)
|
||||
|
||||
travel 1.minute
|
||||
|
||||
expect { described_class.upsert_closing_time!(user, object) }
|
||||
.to change { existing.reload.updated_at }
|
||||
end
|
||||
|
||||
it 'does not create a new recent close if a matching one already exists' do
|
||||
create(:recent_close, user:, recently_closed_object: object)
|
||||
|
||||
expect { described_class.upsert_closing_time!(user, object) }
|
||||
.not_to change(described_class, :count)
|
||||
end
|
||||
|
||||
it 'creates a new recent close for the different user' do
|
||||
create(:recent_close, user: create(:user), recently_closed_object: object)
|
||||
|
||||
expect { described_class.upsert_closing_time!(user, object) }
|
||||
.to change(described_class, :count).by(1)
|
||||
end
|
||||
|
||||
it 'creates a new recent close for the different object' do
|
||||
create(:recent_close, user:, recently_closed_object: create(:ticket))
|
||||
|
||||
expect { described_class.upsert_closing_time!(user, object) }
|
||||
.to change(described_class, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.cleanup' do
|
||||
it 'deletes old recent closes' do
|
||||
create(:recent_close)
|
||||
travel 5.weeks
|
||||
create(:recent_close)
|
||||
|
||||
travel 2.months
|
||||
|
||||
expect { described_class.cleanup }.to change(described_class, :count).by(-1)
|
||||
end
|
||||
|
||||
it 'keeps old recent closes that were bumped recently' do
|
||||
old_close = create(:recent_close)
|
||||
travel 5.weeks
|
||||
create(:recent_close)
|
||||
|
||||
travel 2.months
|
||||
|
||||
old_close.touch
|
||||
|
||||
expect { described_class.cleanup }.not_to change(described_class, :count)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#trigger_subscriptions' do
|
||||
let(:user) { create(:agent) }
|
||||
|
||||
it 'triggers subscription after creating' do
|
||||
allow(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to receive(:trigger)
|
||||
|
||||
described_class.upsert_closing_time!(user, create(:ticket))
|
||||
|
||||
expect(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to have_received(:trigger)
|
||||
.with({}, scope: user.id)
|
||||
end
|
||||
|
||||
it 'triggers subscription after updating' do
|
||||
described_class.upsert_closing_time!(user, create(:ticket))
|
||||
|
||||
travel 1.day
|
||||
|
||||
allow(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to receive(:trigger)
|
||||
|
||||
described_class.upsert_closing_time!(user, create(:ticket))
|
||||
|
||||
expect(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to have_received(:trigger)
|
||||
.with({}, scope: user.id)
|
||||
end
|
||||
|
||||
it 'trigger subscription after destroying' do
|
||||
recent_close = described_class.upsert_closing_time!(user, create(:ticket))
|
||||
|
||||
allow(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to receive(:trigger)
|
||||
|
||||
recent_close.destroy
|
||||
|
||||
expect(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to have_received(:trigger)
|
||||
.with({}, scope: user.id)
|
||||
end
|
||||
|
||||
it 'does not trigger subscription after deleting' do
|
||||
recent_close = described_class.upsert_closing_time!(user, create(:ticket))
|
||||
|
||||
allow(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to receive(:trigger)
|
||||
|
||||
recent_close.delete
|
||||
|
||||
expect(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.not_to have_received(:trigger)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.destroy_logs' do
|
||||
it 'deletes recent closes for the given object' do
|
||||
ticket1 = create(:ticket)
|
||||
ticket2 = create(:ticket)
|
||||
user1 = create(:agent)
|
||||
user2 = create(:agent)
|
||||
_recent_close1 = create(:recent_close, recently_closed_object: ticket1, user: user1)
|
||||
_recent_close2 = create(:recent_close, recently_closed_object: ticket1, user: user2)
|
||||
recent_close3 = create(:recent_close, recently_closed_object: ticket2, user: user2)
|
||||
|
||||
described_class.destroy_logs(ticket1)
|
||||
|
||||
expect(described_class.all).to contain_exactly(recent_close3)
|
||||
end
|
||||
|
||||
it 'triggers push notification with correct scope' do
|
||||
ticket = create(:ticket)
|
||||
user = create(:agent)
|
||||
create(:recent_close, recently_closed_object: ticket, user: user)
|
||||
|
||||
allow(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to receive(:trigger)
|
||||
|
||||
described_class.destroy_logs(ticket)
|
||||
|
||||
expect(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to have_received(:trigger)
|
||||
.with({}, scope: user.id)
|
||||
end
|
||||
|
||||
it 'triggers push notification for each user' do
|
||||
ticket = create(:ticket)
|
||||
user1 = create(:agent)
|
||||
user2 = create(:agent)
|
||||
create(:recent_close, recently_closed_object: ticket, user: user1)
|
||||
create(:recent_close, recently_closed_object: ticket, user: user2)
|
||||
create(:recent_close, recently_closed_object: create(:ticket), user: user2)
|
||||
|
||||
allow(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to receive(:trigger)
|
||||
|
||||
described_class.destroy_logs(ticket)
|
||||
|
||||
expect(Gql::Subscriptions::User::Current::RecentClose::Updates)
|
||||
.to have_received(:trigger)
|
||||
.twice
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -576,4 +576,33 @@ RSpec.describe Taskbar, performs_jobs: true, type: :model do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#log_recent_close' do
|
||||
before do
|
||||
allow(RecentClose).to receive(:upsert_closing_time!)
|
||||
end
|
||||
|
||||
context 'when taskbar has a related object' do
|
||||
let(:ticket) { create(:ticket) }
|
||||
let(:user) { create(:agent) }
|
||||
let(:taskbar) { create(:taskbar, :with_ticket, ticket:, user:) }
|
||||
|
||||
it 'calls RecentClose.upsert_closing_time!' do
|
||||
taskbar.destroy
|
||||
|
||||
expect(RecentClose).to have_received(:upsert_closing_time!).with(user, ticket)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when taskbar has no related object' do
|
||||
let(:user) { create(:agent) }
|
||||
let(:taskbar) { create(:taskbar, user:) }
|
||||
|
||||
it 'deos not call RecentClose.upsert_closing_time!' do
|
||||
taskbar.destroy
|
||||
|
||||
expect(RecentClose).not_to have_received(:upsert_closing_time!)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@ require 'models/concerns/checks_core_workflow_examples'
|
||||
require 'models/concerns/has_history_examples'
|
||||
require 'models/concerns/has_tags_examples'
|
||||
require 'models/concerns/has_taskbars_examples'
|
||||
require 'models/concerns/has_recent_closes_examples'
|
||||
require 'models/concerns/has_xss_sanitized_note_examples'
|
||||
require 'models/concerns/has_object_manager_attributes_examples'
|
||||
require 'models/tag/writes_to_ticket_history_examples'
|
||||
@@ -30,6 +31,7 @@ RSpec.describe Ticket, type: :model do
|
||||
it_behaves_like 'HasTags'
|
||||
it_behaves_like 'TagWritesToTicketHistory'
|
||||
it_behaves_like 'HasTaskbars'
|
||||
it_behaves_like 'HasRecentCloses'
|
||||
it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
|
||||
it_behaves_like 'HasObjectManagerAttributes'
|
||||
it_behaves_like 'Ticket::Escalation'
|
||||
|
||||
@@ -15,6 +15,7 @@ require 'models/concerns/has_object_manager_attributes_examples'
|
||||
require 'models/user/can_lookup_search_index_attributes_examples'
|
||||
require 'models/user/performs_geo_lookup_examples'
|
||||
require 'models/concerns/has_taskbars_examples'
|
||||
require 'models/concerns/has_recent_closes_examples'
|
||||
require 'models/concerns/has_two_factor_examples'
|
||||
|
||||
RSpec.describe User, type: :model do
|
||||
@@ -39,6 +40,7 @@ RSpec.describe User, type: :model do
|
||||
it_behaves_like 'HasObjectManagerAttributes'
|
||||
it_behaves_like 'CanLookupSearchIndexAttributes'
|
||||
it_behaves_like 'HasTaskbars'
|
||||
it_behaves_like 'HasRecentCloses'
|
||||
it_behaves_like 'UserPerformsGeoLookup'
|
||||
it_behaves_like 'Association clears cache', association: :roles
|
||||
it_behaves_like 'Association clears cache', association: :organizations
|
||||
@@ -712,6 +714,7 @@ RSpec.describe User, type: :model do
|
||||
'Chat::Agent' => { 'created_by_id' => 1, 'updated_by_id' => 1 },
|
||||
'Chat::Session' => { 'user_id' => 1, 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'Tag' => { 'created_by_id' => 0 },
|
||||
'RecentClose' => { 'user_id' => 0 },
|
||||
'RecentView' => { 'created_by_id' => 1 },
|
||||
'KnowledgeBase::Answer::Translation' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
'LdapSource' => { 'created_by_id' => 0, 'updated_by_id' => 0 },
|
||||
|
||||
44
spec/services/service/user/list_recent_closes_spec.rb
Normal file
44
spec/services/service/user/list_recent_closes_spec.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Service::User::ListRecentCloses do
|
||||
describe '#execute' do
|
||||
let(:group) { create(:group) }
|
||||
let(:user) { create(:agent, groups: [group]) }
|
||||
let(:ticket1) { create(:ticket, group:) }
|
||||
let(:ticket2) { create(:ticket, group:) }
|
||||
let(:ticket3) { create(:ticket, group:) }
|
||||
let(:organization) { create(:organization) }
|
||||
let(:closed_user) { create(:user) }
|
||||
|
||||
context 'with many recent closes' do
|
||||
before do
|
||||
create(:recent_close, user:, recently_closed_object: ticket1, updated_at: 2.days.ago)
|
||||
create(:recent_close, user:, recently_closed_object: organization, updated_at: 1.day.ago)
|
||||
create(:recent_close, user:, recently_closed_object: ticket2, updated_at: 3.days.ago)
|
||||
create(:recent_close, user:, recently_closed_object: ticket3, updated_at: 3.hours.ago)
|
||||
create(:recent_close, user:, recently_closed_object: closed_user, updated_at: 1.week.ago)
|
||||
end
|
||||
|
||||
it 'returns recently closed objects in correct order' do
|
||||
result = described_class.new(user).execute
|
||||
|
||||
expect(result).to eq([ticket3, organization, ticket1, ticket2, closed_user])
|
||||
end
|
||||
|
||||
it 'respects the limit parameter' do
|
||||
result = described_class.new(user, limit: 2).execute
|
||||
|
||||
expect(result).to eq([ticket3, organization])
|
||||
end
|
||||
|
||||
it 'returns only objects the user has access to' do
|
||||
expect { ticket2.update!(group: create(:group)) }
|
||||
.to change { described_class.new(user).execute }
|
||||
.from([ticket3, organization, ticket1, ticket2, closed_user])
|
||||
.to([ticket3, organization, ticket1, closed_user])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user