refactor(appstore): adjust frontend for new API location

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-04-29 19:45:49 +02:00
parent 3f8710500c
commit 2e0b001a41
9 changed files with 86 additions and 62 deletions

View file

@ -38,13 +38,13 @@ import { mdiEyeOffOutline } from '@mdi/js'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { generateOcsUrl } from '@nextcloud/router'
import { defineAsyncComponent, defineComponent, onBeforeMount, ref } from 'vue'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import logger from '../../utils/logger.ts'
import { filterElements, parseApiResponse } from '../../utils/appDiscoverParser.ts'
import logger from '../../utils/logger.ts'
const PostType = defineAsyncComponent(() => import('./PostType.vue'))
const CarouselType = defineAsyncComponent(() => import('./CarouselType.vue'))

View file

@ -66,7 +66,7 @@ import type { PropType } from 'vue'
import type { IAppDiscoverPost } from '../../constants/AppDiscoverTypes.ts'
import { mdiPlayCircleOutline } from '@mdi/js'
import { generateUrl } from '@nextcloud/router'
import { generateOcsUrl } from '@nextcloud/router'
import { useElementSize, useElementVisibility } from '@vueuse/core'
import { computed, defineComponent, ref, watchEffect } from 'vue'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
@ -144,7 +144,9 @@ export default defineComponent({
*
* @param url The URL to resolve
*/
const generatePrivacyUrl = (url: string) => url.startsWith('/') ? url : generateUrl('/settings/api/apps/media?fileName={fileName}', { fileName: url })
const generatePrivacyUrl = (url: string) => url.startsWith('/')
? url
: generateOcsUrl('/apps/appstore/api/v1/discover/media?fileName={fileName}', { fileName: url })
const mediaElement = ref<HTMLVideoElement | HTMLPictureElement>()
const mediaIsVisible = useElementVisibility(mediaElement, { threshold: 0.3 })

View file

@ -4,7 +4,9 @@
*/
import axios from '@nextcloud/axios'
import { confirmPassword } from '@nextcloud/password-confirmation'
import { addPasswordConfirmationInterceptors, confirmPassword } from '@nextcloud/password-confirmation'
addPasswordConfirmationInterceptors(axios)
/**
* @param {string} url - The url to sanitize
@ -52,8 +54,8 @@ export default {
get(url, options) {
return axios.get(sanitize(url), options)
},
post(url, data) {
return axios.post(sanitize(url), data)
post(url, data, options) {
return axios.post(sanitize(url), data, options)
},
patch(url, data) {
return axios.patch(sanitize(url), data)

View file

@ -3,13 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { OCSResponse } from '@nextcloud/typings/ocs'
import type { IAppstoreApp, IAppstoreCategory } from '../app-types.ts'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { generateOcsUrl } from '@nextcloud/router'
import { defineStore } from 'pinia'
import APPSTORE_CATEGORY_ICONS from '../constants/AppstoreCategoryIcons.ts'
import logger from '../utils/logger.ts'
@ -37,8 +38,10 @@ export const useAppsStore = defineStore('appstore-apps', {
try {
this.loading.categories = true
const { data: categories } = await axios.get<IAppstoreCategory[]>(generateUrl('settings/apps/categories'))
const url = generateOcsUrl('apps/appstore/api/v1/apps/categories')
const { data } = await axios.get<OCSResponse<IAppstoreCategory[]>>(url)
const categories = data.ocs.data
for (const category of categories) {
category.icon = APPSTORE_CATEGORY_ICONS[category.id] ?? ''
}
@ -61,10 +64,11 @@ export const useAppsStore = defineStore('appstore-apps', {
try {
this.loading.apps = true
const { data } = await axios.get<{ apps: IAppstoreApp[] }>(generateUrl('settings/apps/list'))
const url = generateOcsUrl('apps/appstore/api/v1/apps')
const { data } = await axios.get<OCSResponse<IAppstoreApp[]>>(url)
this.$patch({
apps: data.apps,
apps: data.ocs.data,
})
} catch (error) {
logger.error(error as Error)

View file

@ -7,7 +7,7 @@ import axios from '@nextcloud/axios'
import { showError, showInfo } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { PwdConfirmationMode } from '@nextcloud/password-confirmation'
import { generateUrl } from '@nextcloud/router'
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
import Vue from 'vue'
import logger from '../utils/logger.ts'
import api from './api.js'
@ -196,7 +196,9 @@ const actions = {
}
})
return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups }, { confirmPassword: PwdConfirmationMode.Strict })
const url = generateOcsUrl('apps/appstore/api/v1/apps/enable')
return Promise.all(apps.map((appId) => api
.post(url, { appId, groups }, { confirmPassword: PwdConfirmationMode.Strict })
.then((response) => {
context.commit('stopLoading', apps)
context.commit('stopLoading', 'install')
@ -256,7 +258,7 @@ const actions = {
})
context.commit('APPS_API_FAILURE', { appId, error })
}
})
})))
},
forceEnableApp(context, { appId }) {
let apps
@ -268,7 +270,8 @@ const actions = {
return api.requireAdmin().then(() => {
context.commit('startLoading', apps)
context.commit('startLoading', 'install')
return api.post(generateUrl('settings/apps/force'), { appId })
const url = generateOcsUrl('apps/appstore/api/v1/apps/enable')
return api.post(url, { appId, force: true }, { confirmPassword: PwdConfirmationMode.Strict })
.then(() => {
context.commit('setInstallState', { appId, canInstall: true })
})
@ -296,24 +299,28 @@ const actions = {
}
return api.requireAdmin().then(() => {
context.commit('startLoading', apps)
return api.post(generateUrl('settings/apps/disable'), { appIds: apps })
.then(() => {
context.commit('stopLoading', apps)
apps.forEach((_appId) => {
context.commit('disableApp', _appId)
const url = generateOcsUrl('apps/appstore/api/v1/apps/disable')
return Promise.all(apps.map((appId) => {
return api.post(url, { appId })
.then(() => {
context.commit('stopLoading', apps)
apps.forEach((_appId) => {
context.commit('disableApp', _appId)
})
return true
})
return true
})
.catch((error) => {
context.commit('stopLoading', apps)
context.commit('APPS_API_FAILURE', { appId, error })
})
.catch((error) => {
context.commit('stopLoading', apps)
context.commit('APPS_API_FAILURE', { appId, error })
})
}))
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
},
uninstallApp(context, { appId }) {
return api.requireAdmin().then(() => {
context.commit('startLoading', appId)
return api.get(generateUrl(`settings/apps/uninstall/${appId}`))
const url = generateOcsUrl('apps/appstore/api/v1/apps/uninstall')
return api.post(url, { appId })
.then(() => {
context.commit('stopLoading', appId)
context.commit('uninstallApp', appId)
@ -330,7 +337,8 @@ const actions = {
return api.requireAdmin().then(() => {
context.commit('startLoading', appId)
context.commit('startLoading', 'install')
return api.get(generateUrl(`settings/apps/update/${appId}`))
const url = generateOcsUrl('apps/appstore/api/v1/apps/update')
return api.post(url, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
.then(() => {
context.commit('stopLoading', 'install')
context.commit('stopLoading', appId)
@ -347,9 +355,11 @@ const actions = {
getAllApps(context) {
context.commit('startLoading', 'list')
return api.get(generateUrl('settings/apps/list'))
const url = generateOcsUrl('apps/appstore/api/v1/apps')
return api.get(url)
.then((response) => {
context.commit('setAllApps', response.data.apps)
const apps = response.data.ocs.data
context.commit('setAllApps', apps)
context.commit('stopLoading', 'list')
return true
})
@ -360,9 +370,9 @@ const actions = {
if (shouldRefetchCategories || !context.state.gettingCategoriesPromise) {
context.commit('startLoading', 'categories')
try {
const categoriesPromise = api.get(generateUrl('settings/apps/categories'))
const categoriesPromise = api.get(generateOcsUrl('apps/appstore/api/v1/apps/categories'))
context.commit('updateCategories', categoriesPromise)
const categoriesPromiseResponse = await categoriesPromise
const categoriesPromiseResponse = (await categoriesPromise).data.ocs
if (categoriesPromiseResponse.data.length > 0) {
context.commit('appendCategories', categoriesPromiseResponse.data)
context.commit('stopLoading', 'categories')

View file

@ -4,7 +4,7 @@
*/
import { User } from '@nextcloud/e2e-test-server/cypress'
import { handlePasswordConfirmation } from './usersUtils.ts'
import { handlePasswordConfirmation } from '../core-utils.ts'
const admin = new User('admin', 'admin')
@ -19,7 +19,7 @@ describe('Settings: App management', { testIsolation: true }, () => {
cy.login(admin)
// Intercept the apps list request
cy.intercept('GET', '*/settings/apps/list').as('fetchAppsList')
cy.intercept('GET', '**/ocs/v2.php/apps/appstore/api/v1/apps').as('fetchAppsList')
// I open the Apps management
cy.visit('/settings/apps/installed')
@ -29,6 +29,7 @@ describe('Settings: App management', { testIsolation: true }, () => {
})
it('Can enable an installed app', () => {
cy.intercept('POST', '**/ocs/v2.php/apps/appstore/api/v1/apps/enable').as('enableApp')
cy.get('#apps-list').should('exist')
// Wait for the app list to load
.contains('tr', 'QA testing', { timeout: 10000 })
@ -38,6 +39,7 @@ describe('Settings: App management', { testIsolation: true }, () => {
.click({ force: true })
handlePasswordConfirmation(admin.password)
cy.wait('@enableApp')
// Wait until we see the disable button for the app
cy.get('#apps-list').should('exist')
@ -54,6 +56,7 @@ describe('Settings: App management', { testIsolation: true }, () => {
})
it('Can disable an installed app', () => {
cy.intercept('POST', '**/ocs/v2.php/apps/appstore/api/v1/apps/disable').as('disableApp')
cy.get('#apps-list')
.should('exist')
// Wait for the app list to load
@ -64,6 +67,7 @@ describe('Settings: App management', { testIsolation: true }, () => {
.click({ force: true })
handlePasswordConfirmation(admin.password)
cy.wait('@disableApp')
// Wait until we see the disable button for the app
cy.get('#apps-list').should('exist')
@ -137,12 +141,11 @@ describe('Settings: App management', { testIsolation: true }, () => {
.find('.app-sidebar-header__info')
.should('contain', 'QA testing')
cy.get('#app-sidebar-vue').contains('a', 'View in store').should('exist')
cy.get('#app-sidebar-vue').find('input[type="button"][value="Enable"]').should('be.visible')
cy.get('#app-sidebar-vue').find('input[type="button"][value="Remove"]').should('be.visible')
cy.get('#app-sidebar-vue').findByRole('button', { name: 'Enable' }).should('be.visible')
cy.get('#app-sidebar-vue').contains(/Version \d+\.\d+\.\d+/).should('be.visible')
})
it('Limit app usage to group', () => {
it.skip('Limit app usage to group', () => {
// When I open the "Active apps" section
cy.get('#app-category-enabled a')
.should('contain', 'Active apps')

View file

@ -10,6 +10,32 @@ export function getUnifiedSearchModal() {
return cy.get('#unified-search')
}
/**
* Handle the confirm password dialog (if needed)
*
* @param adminPassword The admin password for the dialog
*/
export function handlePasswordConfirmation(adminPassword = 'admin') {
const handleModal = (context: Cypress.Chainable) => {
return context.contains('.modal-container', 'Authentication required')
.if()
.within(() => {
cy.get('input[type="password"]')
.type(adminPassword)
cy.findByRole('button', { name: 'Confirm' })
.click()
})
}
return cy.get('body')
.if()
.then(() => handleModal(cy.get('body')))
.else()
// Handle if inside a cy.within
.root().closest('body')
.then(($body) => handleModal(cy.wrap($body)))
}
/**
* Open the unified search modal
*/

View file

@ -73,28 +73,4 @@ export function saveEditDialog() {
cy.get('.edit-dialog').should('not.exist')
}
/**
* Handle the confirm password dialog (if needed)
*
* @param adminPassword The admin password for the dialog
*/
export function handlePasswordConfirmation(adminPassword = 'admin') {
const handleModal = (context: Cypress.Chainable) => {
return context.contains('.modal-container', 'Authentication required')
.if()
.within(() => {
cy.get('input[type="password"]')
.type(adminPassword)
cy.findByRole('button', { name: 'Confirm' })
.click()
})
}
return cy.get('body')
.if()
.then(() => handleModal(cy.get('body')))
.else()
// Handle if inside a cy.within
.root().closest('body')
.then(($body) => handleModal(cy.wrap($body)))
}
export { handlePasswordConfirmation } from '../core-utils.ts'

View file

@ -471,6 +471,7 @@ class OC_App {
}
$info['version'] = $appManager->getAppVersion($app);
$info['license'] ??= $info['licence'];
$appList[] = $info;
}
}