Skip to content

Commit c69d1c5

Browse files
authored
Merge pull request #44282 from nextcloud/fix/app-discover-pin-and-parse
fix(settings): Support `order` property on App Discover elements and hide future elements
2 parents 174c10a + d5b1de8 commit c69d1c5

8 files changed

Lines changed: 139 additions & 14 deletions

apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js
3838
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
3939
4040
import logger from '../../logger'
41-
import { apiTypeParser } from '../../utils/appDiscoverTypeParser.ts'
41+
import { parseApiResponse, filterElements } from '../../utils/appDiscoverParser.ts'
4242
4343
const PostType = defineAsyncComponent(() => import('./PostType.vue'))
4444
const CarouselType = defineAsyncComponent(() => import('./CarouselType.vue'))
@@ -50,7 +50,7 @@ const elements = ref<IAppDiscoverElements[]>([])
5050
* Shuffle using the Fisher-Yates algorithm
5151
* @param array The array to shuffle (in place)
5252
*/
53-
const shuffleArray = (array) => {
53+
const shuffleArray = <T, >(array: T[]): T[] => {
5454
for (let i = array.length - 1; i > 0; i--) {
5555
const j = Math.floor(Math.random() * (i + 1));
5656
[array[i], array[j]] = [array[j], array[i]]
@@ -64,8 +64,14 @@ const shuffleArray = (array) => {
6464
onBeforeMount(async () => {
6565
try {
6666
const { data } = await axios.get<Record<string, unknown>[]>(generateUrl('/settings/api/apps/discover'))
67-
const parsedData = data.map(apiTypeParser)
68-
elements.value = shuffleArray(parsedData)
67+
// Parse data to ensure dates are useable and then filter out expired or future elements
68+
const parsedElements = data.map(parseApiResponse).filter(filterElements)
69+
// Shuffle elements to make it looks more interesting
70+
const shuffledElements = shuffleArray(parsedElements)
71+
// Sort pinned elements first
72+
shuffledElements.sort((a, b) => (a.order ?? Infinity) < (b.order ?? Infinity) ? -1 : 1)
73+
// Set the elements to the UI
74+
elements.value = shuffledElements
6975
} catch (error) {
7076
hasError.value = true
7177
logger.error(error as Error)

apps/settings/src/constants/AppDiscoverTypes.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ export interface IAppDiscoverElement {
4141
*/
4242
id: string,
4343

44+
/**
45+
* Order of this element to pin elements (smaller = shown on top)
46+
*/
47+
order?: number
48+
4449
/**
4550
* Optional, localized, headline for the element
4651
*/
@@ -54,12 +59,12 @@ export interface IAppDiscoverElement {
5459
/**
5560
* Optional date when this element will get valid (only show since then)
5661
*/
57-
date?: Date|number
62+
date?: number
5863

5964
/**
6065
* Optional date when this element will be invalid (only show until then)
6166
*/
62-
expiryDate?: Date|number
67+
expiryDate?: number
6368
}
6469

6570
/** Wrapper for media source and MIME type */
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
3+
*
4+
* @author Ferdinand Thiessen <opensource@fthiessen.de>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import type { IAppDiscoverElement } from '../constants/AppDiscoverTypes'
24+
25+
import { describe, expect, it } from '@jest/globals'
26+
import { filterElements, parseApiResponse } from './appDiscoverParser'
27+
28+
describe('App Discover API parser', () => {
29+
describe('filterElements', () => {
30+
it('can filter expired elements', () => {
31+
const result = filterElements({ id: 'test', type: 'post', expiryDate: 100 })
32+
expect(result).toBe(false)
33+
})
34+
35+
it('can filter upcoming elements', () => {
36+
const result = filterElements({ id: 'test', type: 'post', date: Date.now() + 10000 })
37+
expect(result).toBe(false)
38+
})
39+
40+
it('ignores element without dates', () => {
41+
const result = filterElements({ id: 'test', type: 'post' })
42+
expect(result).toBe(true)
43+
})
44+
45+
it('allows not yet expired elements', () => {
46+
const result = filterElements({ id: 'test', type: 'post', expiryDate: Date.now() + 10000 })
47+
expect(result).toBe(true)
48+
})
49+
50+
it('allows yet included elements', () => {
51+
const result = filterElements({ id: 'test', type: 'post', date: 100 })
52+
expect(result).toBe(true)
53+
})
54+
55+
it('allows elements included and not expired', () => {
56+
const result = filterElements({ id: 'test', type: 'post', date: 100, expiryDate: Date.now() + 10000 })
57+
expect(result).toBe(true)
58+
})
59+
60+
it('can handle null values', () => {
61+
const result = filterElements({ id: 'test', type: 'post', date: null, expiryDate: null } as unknown as IAppDiscoverElement)
62+
expect(result).toBe(true)
63+
})
64+
})
65+
66+
describe('parseApiResponse', () => {
67+
it('can handle basic post', () => {
68+
const result = parseApiResponse({ id: 'test', type: 'post' })
69+
expect(result).toEqual({ id: 'test', type: 'post' })
70+
})
71+
72+
it('can handle carousel', () => {
73+
const result = parseApiResponse({ id: 'test', type: 'carousel' })
74+
expect(result).toEqual({ id: 'test', type: 'carousel' })
75+
})
76+
77+
it('can handle showcase', () => {
78+
const result = parseApiResponse({ id: 'test', type: 'showcase' })
79+
expect(result).toEqual({ id: 'test', type: 'showcase' })
80+
})
81+
82+
it('throws on unknown type', () => {
83+
expect(() => parseApiResponse({ id: 'test', type: 'foo-bar' })).toThrow()
84+
})
85+
86+
it('parses the date', () => {
87+
const result = parseApiResponse({ id: 'test', type: 'showcase', date: '2024-03-19T17:28:19+0000' })
88+
expect(result).toEqual({ id: 'test', type: 'showcase', date: 1710869299000 })
89+
})
90+
91+
it('parses the expiryDate', () => {
92+
const result = parseApiResponse({ id: 'test', type: 'showcase', expiryDate: '2024-03-19T17:28:19Z' })
93+
expect(result).toEqual({ id: 'test', type: 'showcase', expiryDate: 1710869299000 })
94+
})
95+
})
96+
})

apps/settings/src/utils/appDiscoverTypeParser.ts renamed to apps/settings/src/utils/appDiscoverParser.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020
*
2121
*/
2222

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

2525
/**
2626
* Helper to transform the JSON API results to proper frontend objects (app discover section elements)
2727
*
2828
* @param element The JSON API element to transform
2929
*/
30-
export const apiTypeParser = (element: Record<string, unknown>): IAppDiscoverElements => {
30+
export const parseApiResponse = (element: Record<string, unknown>): IAppDiscoverElements => {
3131
const appElement = { ...element }
3232
if (appElement.date) {
3333
appElement.date = Date.parse(appElement.date as string)
@@ -45,3 +45,21 @@ export const apiTypeParser = (element: Record<string, unknown>): IAppDiscoverEle
4545
}
4646
throw new Error(`Invalid argument, app discover element with type ${element.type ?? 'unknown'} is unknown`)
4747
}
48+
49+
/**
50+
* Filter outdated or upcoming elements
51+
* @param element Element to check
52+
*/
53+
export const filterElements = (element: IAppDiscoverElement) => {
54+
const now = Date.now()
55+
// Element not yet published
56+
if (element.date && element.date > now) {
57+
return false
58+
}
59+
60+
// Element expired
61+
if (element.expiryDate && element.expiryDate < now) {
62+
return false
63+
}
64+
return true
65+
}

dist/settings-apps-view-4529.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-apps-view-4529.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-vue-settings-apps-users-management.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-vue-settings-apps-users-management.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)