summaryrefslogtreecommitdiffstats
path: root/apps/settings
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-03-18 17:55:26 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2024-03-19 18:30:56 +0100
commit2937fd3eb05dcb8abe03d384d6cc9e6d7a885c9a (patch)
treea0aa62c0c8d8f27ae9f4ab64c29221e585a83855 /apps/settings
parent174c10ab3fd7bf92b7cc509f9405cc8f57848e83 (diff)
downloadnextcloud-server-2937fd3eb05dcb8abe03d384d6cc9e6d7a885c9a.tar.gz
nextcloud-server-2937fd3eb05dcb8abe03d384d6cc9e6d7a885c9a.zip
fix(settings): Support `order` property on App Discover elements and hide future elements
This adds support to pinning elements by setting the `order` property on the element (e.g. `order: 0` will always be the first element to show). Also filter list of elements to remove upcoming and outdated elements (as the json might be cached). Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/settings')
-rw-r--r--apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue14
-rw-r--r--apps/settings/src/constants/AppDiscoverTypes.ts9
-rw-r--r--apps/settings/src/utils/appDiscoverParser.spec.ts96
-rw-r--r--apps/settings/src/utils/appDiscoverParser.ts (renamed from apps/settings/src/utils/appDiscoverTypeParser.ts)22
4 files changed, 133 insertions, 8 deletions
diff --git a/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue b/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue
index 68610347420..4072af9f719 100644
--- a/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue
+++ b/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue
@@ -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'))
@@ -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[] => {
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]]
@@ -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)
diff --git a/apps/settings/src/constants/AppDiscoverTypes.ts b/apps/settings/src/constants/AppDiscoverTypes.ts
index d28516fe79c..07637936fd4 100644
--- a/apps/settings/src/constants/AppDiscoverTypes.ts
+++ b/apps/settings/src/constants/AppDiscoverTypes.ts
@@ -42,6 +42,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
*/
headline?: ILocalizedValue<string>
@@ -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 */
diff --git a/apps/settings/src/utils/appDiscoverParser.spec.ts b/apps/settings/src/utils/appDiscoverParser.spec.ts
new file mode 100644
index 00000000000..e00b24dff49
--- /dev/null
+++ b/apps/settings/src/utils/appDiscoverParser.spec.ts
@@ -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 })
+ })
+ })
+})
diff --git a/apps/settings/src/utils/appDiscoverTypeParser.ts b/apps/settings/src/utils/appDiscoverParser.ts
index ed20138e91b..96f7d3e4b7d 100644
--- a/apps/settings/src/utils/appDiscoverTypeParser.ts
+++ b/apps/settings/src/utils/appDiscoverParser.ts
@@ -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)
@@ -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
+}