Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(settings): Support order property on App Discover elements and hide future elements #44282

Merged
merged 2 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'

import logger from '../../logger'
import { apiTypeParser } from '../../utils/appDiscoverTypeParser.ts'
import { parseApiResponse, filterElements } from '../../utils/appDiscoverParser.ts'

const PostType = defineAsyncComponent(() => import('./PostType.vue'))
const CarouselType = defineAsyncComponent(() => import('./CarouselType.vue'))
Expand All @@ -50,7 +50,7 @@ const elements = ref<IAppDiscoverElements[]>([])
* Shuffle using the Fisher-Yates algorithm
* @param array The array to shuffle (in place)
*/
const shuffleArray = (array) => {
const shuffleArray = <T, >(array: T[]): T[] => {
Pytal marked this conversation as resolved.
Show resolved Hide resolved
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]]
Expand All @@ -64,8 +64,14 @@ const shuffleArray = (array) => {
onBeforeMount(async () => {
try {
const { data } = await axios.get<Record<string, unknown>[]>(generateUrl('/settings/api/apps/discover'))
const parsedData = data.map(apiTypeParser)
elements.value = shuffleArray(parsedData)
// Parse data to ensure dates are useable and then filter out expired or future elements
const parsedElements = data.map(parseApiResponse).filter(filterElements)
// Shuffle elements to make it looks more interesting
const shuffledElements = shuffleArray(parsedElements)
// Sort pinned elements first
shuffledElements.sort((a, b) => (a.order ?? Infinity) < (b.order ?? Infinity) ? -1 : 1)
// Set the elements to the UI
elements.value = shuffledElements
} catch (error) {
hasError.value = true
logger.error(error as Error)
Expand Down
9 changes: 7 additions & 2 deletions apps/settings/src/constants/AppDiscoverTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export interface IAppDiscoverElement {
*/
id: string,

/**
* Order of this element to pin elements (smaller = shown on top)
*/
order?: number

/**
* Optional, localized, headline for the element
*/
Expand All @@ -54,12 +59,12 @@ export interface IAppDiscoverElement {
/**
* Optional date when this element will get valid (only show since then)
*/
date?: Date|number
date?: number

/**
* Optional date when this element will be invalid (only show until then)
*/
expiryDate?: Date|number
expiryDate?: number
}

/** Wrapper for media source and MIME type */
Expand Down
96 changes: 96 additions & 0 deletions apps/settings/src/utils/appDiscoverParser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import type { IAppDiscoverElement } from '../constants/AppDiscoverTypes'

import { describe, expect, it } from '@jest/globals'
import { filterElements, parseApiResponse } from './appDiscoverParser'

describe('App Discover API parser', () => {
describe('filterElements', () => {
it('can filter expired elements', () => {
const result = filterElements({ id: 'test', type: 'post', expiryDate: 100 })
expect(result).toBe(false)
})

it('can filter upcoming elements', () => {
const result = filterElements({ id: 'test', type: 'post', date: Date.now() + 10000 })
expect(result).toBe(false)
})

it('ignores element without dates', () => {
const result = filterElements({ id: 'test', type: 'post' })
expect(result).toBe(true)
})

it('allows not yet expired elements', () => {
const result = filterElements({ id: 'test', type: 'post', expiryDate: Date.now() + 10000 })
expect(result).toBe(true)
})

it('allows yet included elements', () => {
const result = filterElements({ id: 'test', type: 'post', date: 100 })
expect(result).toBe(true)
})

it('allows elements included and not expired', () => {
const result = filterElements({ id: 'test', type: 'post', date: 100, expiryDate: Date.now() + 10000 })
expect(result).toBe(true)
})

it('can handle null values', () => {
const result = filterElements({ id: 'test', type: 'post', date: null, expiryDate: null } as unknown as IAppDiscoverElement)
expect(result).toBe(true)
})
})

describe('parseApiResponse', () => {
it('can handle basic post', () => {
const result = parseApiResponse({ id: 'test', type: 'post' })
expect(result).toEqual({ id: 'test', type: 'post' })
})

it('can handle carousel', () => {
const result = parseApiResponse({ id: 'test', type: 'carousel' })
expect(result).toEqual({ id: 'test', type: 'carousel' })
})

it('can handle showcase', () => {
const result = parseApiResponse({ id: 'test', type: 'showcase' })
expect(result).toEqual({ id: 'test', type: 'showcase' })
})

it('throws on unknown type', () => {
expect(() => parseApiResponse({ id: 'test', type: 'foo-bar' })).toThrow()
})

it('parses the date', () => {
const result = parseApiResponse({ id: 'test', type: 'showcase', date: '2024-03-19T17:28:19+0000' })
expect(result).toEqual({ id: 'test', type: 'showcase', date: 1710869299000 })
})

it('parses the expiryDate', () => {
const result = parseApiResponse({ id: 'test', type: 'showcase', expiryDate: '2024-03-19T17:28:19Z' })
expect(result).toEqual({ id: 'test', type: 'showcase', expiryDate: 1710869299000 })
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
*
*/

import type { IAppDiscoverCarousel, IAppDiscoverElements, IAppDiscoverPost, IAppDiscoverShowcase } from '../constants/AppDiscoverTypes.ts'
import type { IAppDiscoverCarousel, IAppDiscoverElement, IAppDiscoverElements, IAppDiscoverPost, IAppDiscoverShowcase } from '../constants/AppDiscoverTypes.ts'

/**
* Helper to transform the JSON API results to proper frontend objects (app discover section elements)
*
* @param element The JSON API element to transform
*/
export const apiTypeParser = (element: Record<string, unknown>): IAppDiscoverElements => {
export const parseApiResponse = (element: Record<string, unknown>): IAppDiscoverElements => {
const appElement = { ...element }
if (appElement.date) {
appElement.date = Date.parse(appElement.date as string)
Expand All @@ -45,3 +45,21 @@ export const apiTypeParser = (element: Record<string, unknown>): IAppDiscoverEle
}
throw new Error(`Invalid argument, app discover element with type ${element.type ?? 'unknown'} is unknown`)
}

/**
* Filter outdated or upcoming elements
* @param element Element to check
*/
export const filterElements = (element: IAppDiscoverElement) => {
const now = Date.now()
// Element not yet published
if (element.date && element.date > now) {
return false
}

// Element expired
if (element.expiryDate && element.expiryDate < now) {
return false
}
return true
}
4 changes: 2 additions & 2 deletions dist/settings-apps-view-4529.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/settings-apps-view-4529.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/settings-vue-settings-apps-users-management.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/settings-vue-settings-apps-users-management.js.map

Large diffs are not rendered by default.

Loading