--- /dev/null
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { IAppStoreApp, IAppStoreCategory } from '../constants/AppStoreTypes.ts'
+
+import { showError } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t } from '@nextcloud/l10n'
+import { defineStore } from 'pinia'
+
+import { getAllApps, getCategories } from '../service/AppStoreApi.ts'
+import AppStoreCategoryIcons from '../constants/AppStoreCategoryIcons.ts'
+import logger from '../logger'
+import Vue from 'vue'
+
+const showApiError = () => showError(t('settings', 'An error occurred during the request. Unable to proceed.'))
+
+export const useAppStore = defineStore('settings-apps', {
+ state: () => ({
+ apps: [] as IAppStoreApp[],
+ categories: [] as IAppStoreCategory[],
+ updateCount: loadState<number>('settings', 'appstoreUpdateCount', 0),
+ loading: {
+ apps: false,
+ categories: false,
+ },
+ loadingList: false,
+ gettingCategoriesPromise: null,
+ }),
+
+ actions: {
+ /**
+ * Helper to modify an app in the local app store.
+ * @param app The app to modify
+ * @param partialApp Changes to apply to the app
+ */
+ updateApp(app: IAppStoreApp, partialApp: Partial<IAppStoreApp>) {
+ const updatedApp = structuredClone(app)
+ for (const [key, value] of Object.entries(partialApp)) {
+ if (value === undefined) {
+ delete updatedApp[key]
+ } else {
+ updatedApp[key] = value
+ }
+ }
+
+ const index = this.apps.findIndex((a) => a.id === app.id)
+ Vue.set(this.apps, index, updatedApp)
+ },
+
+ async loadCategories(force = false) {
+ if (this.categories.length > 0 && !force) {
+ return
+ }
+
+ try {
+ this.loading.categories = true
+ const categories = await getCategories()
+
+ for (const category of categories) {
+ category.icon = AppStoreCategoryIcons[category.id] ?? ''
+ }
+
+ this.categories = categories
+ } catch (error) {
+ logger.error(error as Error)
+ showApiError()
+ } finally {
+ this.loading.categories = false
+ }
+ },
+
+ async loadApps(force = false) {
+ if (this.apps.length > 0 && !force) {
+ return
+ }
+
+ try {
+ this.loading.apps = true
+ const apps = await getAllApps()
+ this.apps = apps
+ } catch (error) {
+ logger.error(error as Error)
+ showApiError()
+ } finally {
+ this.loading.apps = false
+ }
+ },
+
+ getCategoryById(categoryId: string) {
+ return this.categories.find(({ id }) => id === categoryId) ?? null
+ },
+
+ /**
+ * Get an app by their app id
+ * @param appId The app id to search
+ */
+ getAppById(appId: string): IAppStoreApp|null {
+ return this.apps.find(({ id }) => id === appId) ?? null
+ },
+
+ /**
+ * Get all apps that are part of the specified bundle
+ * @param bundleId The bundle id to filter
+ */
+ getAppsByBundle(bundleId: string): IAppStoreApp[] {
+ return this.apps.filter((app) => app.bundleIds && app.bundleIds.includes(bundleId))
+ },
+
+ /**
+ * Get all apps that are listed in the specified category
+ * @param categoryId The category id to filter
+ */
+ getAppsByCategory(categoryId: string): IAppStoreApp[] {
+ // Also handle special categories
+ switch (categoryId) {
+ case 'enabled':
+ return this.apps.filter((app) => app.active)
+ break
+ case 'disabled':
+ return this.apps.filter((app) => app.installed && !app.active)
+ break
+ case 'updates':
+ return this.apps.filter((app) => app.update)
+ break
+ case 'installed':
+ return this.apps.filter((app) => app.installed)
+ break
+ case 'featured':
+ return this.apps.filter((app) => app.level === 200)
+ break
+ case 'supported':
+ return this.apps.filter((app) => app.level === 300)
+ default:
+ return this.apps.filter((app) => app.category === categoryId || app.category.includes(categoryId))
+ }
+ },
+ },
+})
+++ /dev/null
-/**
- * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-import api from './api.js'
-import Vue from 'vue'
-import { generateUrl } from '@nextcloud/router'
-import { showError, showInfo } from '@nextcloud/dialogs'
-import { loadState } from '@nextcloud/initial-state'
-
-const state = {
- apps: [],
- bundles: loadState('settings', 'appstoreBundles', []),
- categories: [],
- updateCount: loadState('settings', 'appstoreUpdateCount', 0),
- loading: {},
- gettingCategoriesPromise: null,
-}
-
-const mutations = {
-
- APPS_API_FAILURE(state, error) {
- showError(t('settings', 'An error occurred during the request. Unable to proceed.') + '<br>' + error.error.response.data.data.message, { isHTML: true })
- console.error(state, error)
- },
-
- initCategories(state, { categories, updateCount }) {
- state.categories = categories
- state.updateCount = updateCount
- },
-
- updateCategories(state, categoriesPromise) {
- state.gettingCategoriesPromise = categoriesPromise
- },
-
- setUpdateCount(state, updateCount) {
- state.updateCount = updateCount
- },
-
- addCategory(state, category) {
- state.categories.push(category)
- },
-
- appendCategories(state, categoriesArray) {
- // convert obj to array
- state.categories = categoriesArray
- },
-
- setAllApps(state, apps) {
- state.apps = apps
- },
-
- setError(state, { appId, error }) {
- if (!Array.isArray(appId)) {
- appId = [appId]
- }
- appId.forEach((_id) => {
- const app = state.apps.find(app => app.id === _id)
- app.error = error
- })
- },
-
- clearError(state, { appId, error }) {
- const app = state.apps.find(app => app.id === appId)
- app.error = null
- },
-
- enableApp(state, { appId, groups }) {
- const app = state.apps.find(app => app.id === appId)
- app.active = true
- app.groups = groups
- },
-
- setInstallState(state, { appId, canInstall }) {
- const app = state.apps.find(app => app.id === appId)
- if (app) {
- app.canInstall = canInstall === true
- }
- },
-
- disableApp(state, appId) {
- const app = state.apps.find(app => app.id === appId)
- app.active = false
- app.groups = []
- if (app.removable) {
- app.canUnInstall = true
- }
- },
-
- uninstallApp(state, appId) {
- state.apps.find(app => app.id === appId).active = false
- state.apps.find(app => app.id === appId).groups = []
- state.apps.find(app => app.id === appId).needsDownload = true
- state.apps.find(app => app.id === appId).installed = false
- state.apps.find(app => app.id === appId).canUnInstall = false
- state.apps.find(app => app.id === appId).canInstall = true
- },
-
- updateApp(state, appId) {
- const app = state.apps.find(app => app.id === appId)
- const version = app.update
- app.update = null
- app.version = version
- state.updateCount--
-
- },
-
- resetApps(state) {
- state.apps = []
- },
- reset(state) {
- state.apps = []
- state.categories = []
- state.updateCount = 0
- },
- startLoading(state, id) {
- if (Array.isArray(id)) {
- id.forEach((_id) => {
- Vue.set(state.loading, _id, true)
- })
- } else {
- Vue.set(state.loading, id, true)
- }
- },
- stopLoading(state, id) {
- if (Array.isArray(id)) {
- id.forEach((_id) => {
- Vue.set(state.loading, _id, false)
- })
- } else {
- Vue.set(state.loading, id, false)
- }
- },
-}
-
-const getters = {
- loading(state) {
- return function(id) {
- return state.loading[id]
- }
- },
- getCategories(state) {
- return state.categories
- },
- getAllApps(state) {
- return state.apps
- },
- getAppBundles(state) {
- return state.bundles
- },
- getUpdateCount(state) {
- return state.updateCount
- },
- getCategoryById: (state) => (selectedCategoryId) => {
- return state.categories.find((category) => category.id === selectedCategoryId)
- },
-}
-
-const actions = {
-
- enableApp(context, { appId, groups }) {
- let apps
- if (Array.isArray(appId)) {
- apps = appId
- } else {
- apps = [appId]
- }
- return api.requireAdmin().then((response) => {
- context.commit('startLoading', apps)
- context.commit('startLoading', 'install')
- return api.post(generateUrl('settings/apps/enable'), { appIds: apps, groups })
- .then((response) => {
- context.commit('stopLoading', apps)
- context.commit('stopLoading', 'install')
- apps.forEach(_appId => {
- context.commit('enableApp', { appId: _appId, groups })
- })
-
- // check for server health
- return api.get(generateUrl('apps/files/'))
- .then(() => {
- if (response.data.update_required) {
- showInfo(
- t(
- 'settings',
- 'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
- ),
- {
- onClick: () => window.location.reload(),
- close: false,
-
- },
- )
- setTimeout(function() {
- location.reload()
- }, 5000)
- }
- })
- .catch(() => {
- if (!Array.isArray(appId)) {
- showError(t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
- context.commit('setError', {
- appId: apps,
- error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'),
- })
- context.dispatch('disableApp', { appId })
- }
- })
- })
- .catch((error) => {
- context.commit('stopLoading', apps)
- context.commit('stopLoading', 'install')
- context.commit('setError', {
- appId: apps,
- error: error.response.data.data.message,
- })
- context.commit('APPS_API_FAILURE', { appId, error })
- })
- }).catch((error) => context.commit('API_FAILURE', { appId, error }))
- },
- forceEnableApp(context, { appId, groups }) {
- let apps
- if (Array.isArray(appId)) {
- apps = appId
- } else {
- apps = [appId]
- }
- return api.requireAdmin().then(() => {
- context.commit('startLoading', apps)
- context.commit('startLoading', 'install')
- return api.post(generateUrl('settings/apps/force'), { appId })
- .then((response) => {
- context.commit('setInstallState', { appId, canInstall: true })
- })
- .catch((error) => {
- context.commit('stopLoading', apps)
- context.commit('stopLoading', 'install')
- context.commit('setError', {
- appId: apps,
- error: error.response.data.data.message,
- })
- context.commit('APPS_API_FAILURE', { appId, error })
- })
- .finally(() => {
- context.commit('stopLoading', apps)
- context.commit('stopLoading', 'install')
- })
- }).catch((error) => context.commit('API_FAILURE', { appId, error }))
- },
- disableApp(context, { appId }) {
- let apps
- if (Array.isArray(appId)) {
- apps = appId
- } else {
- apps = [appId]
- }
- return api.requireAdmin().then((response) => {
- context.commit('startLoading', apps)
- return api.post(generateUrl('settings/apps/disable'), { appIds: apps })
- .then((response) => {
- context.commit('stopLoading', apps)
- apps.forEach(_appId => {
- context.commit('disableApp', _appId)
- })
- return true
- })
- .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((response) => {
- context.commit('startLoading', appId)
- return api.get(generateUrl(`settings/apps/uninstall/${appId}`))
- .then((response) => {
- context.commit('stopLoading', appId)
- context.commit('uninstallApp', appId)
- return true
- })
- .catch((error) => {
- context.commit('stopLoading', appId)
- context.commit('APPS_API_FAILURE', { appId, error })
- })
- }).catch((error) => context.commit('API_FAILURE', { appId, error }))
- },
-
- updateApp(context, { appId }) {
- return api.requireAdmin().then((response) => {
- context.commit('startLoading', appId)
- context.commit('startLoading', 'install')
- return api.get(generateUrl(`settings/apps/update/${appId}`))
- .then((response) => {
- context.commit('stopLoading', 'install')
- context.commit('stopLoading', appId)
- context.commit('updateApp', appId)
- return true
- })
- .catch((error) => {
- context.commit('stopLoading', appId)
- context.commit('stopLoading', 'install')
- context.commit('APPS_API_FAILURE', { appId, error })
- })
- }).catch((error) => context.commit('API_FAILURE', { appId, error }))
- },
-
- getAllApps(context) {
- context.commit('startLoading', 'list')
- return api.get(generateUrl('settings/apps/list'))
- .then((response) => {
- context.commit('setAllApps', response.data.apps)
- context.commit('stopLoading', 'list')
- return true
- })
- .catch((error) => context.commit('API_FAILURE', error))
- },
-
- async getCategories(context, { shouldRefetchCategories = false } = {}) {
- if (shouldRefetchCategories || !context.state.gettingCategoriesPromise) {
- context.commit('startLoading', 'categories')
- try {
- const categoriesPromise = api.get(generateUrl('settings/apps/categories'))
- context.commit('updateCategories', categoriesPromise)
- const categoriesPromiseResponse = await categoriesPromise
- if (categoriesPromiseResponse.data.length > 0) {
- context.commit('appendCategories', categoriesPromiseResponse.data)
- context.commit('stopLoading', 'categories')
- return true
- }
- context.commit('stopLoading', 'categories')
- return false
- } catch (error) {
- context.commit('API_FAILURE', error)
- }
- }
- return context.state.gettingCategoriesPromise
- },
-
-}
-
-export default { state, mutations, getters, actions }