From fce34d6e9d4aaa906eff47628f702f90393f1f02 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 23 Oct 2024 12:38:07 +0200 Subject: [PATCH] refactor(app-store): Merge both app related stores into one Pinia store Signed-off-by: Ferdinand Thiessen --- apps/settings/src/store/appStore.ts | 141 +++++++++++ apps/settings/src/store/apps-store.ts | 87 ------- apps/settings/src/store/apps.js | 343 -------------------------- apps/settings/src/store/index.js | 2 - 4 files changed, 141 insertions(+), 432 deletions(-) create mode 100644 apps/settings/src/store/appStore.ts delete mode 100644 apps/settings/src/store/apps-store.ts delete mode 100644 apps/settings/src/store/apps.js diff --git a/apps/settings/src/store/appStore.ts b/apps/settings/src/store/appStore.ts new file mode 100644 index 00000000000..b9f015a13c4 --- /dev/null +++ b/apps/settings/src/store/appStore.ts @@ -0,0 +1,141 @@ +/** + * 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('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) { + 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)) + } + }, + }, +}) diff --git a/apps/settings/src/store/apps-store.ts b/apps/settings/src/store/apps-store.ts deleted file mode 100644 index eaf8a0d7f86..00000000000 --- a/apps/settings/src/store/apps-store.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { IAppstoreApp, IAppstoreCategory } from '../app-types.ts' - -import { showError } from '@nextcloud/dialogs' -import { loadState } from '@nextcloud/initial-state' -import { translate as t } from '@nextcloud/l10n' -import { generateUrl } from '@nextcloud/router' -import { defineStore } from 'pinia' - -import axios from '@nextcloud/axios' - -import logger from '../logger' -import APPSTORE_CATEGORY_ICONS from '../constants/AppstoreCategoryIcons.ts' - -const showApiError = () => showError(t('settings', 'An error occurred during the request. Unable to proceed.')) - -export const useAppsStore = defineStore('settings-apps', { - state: () => ({ - apps: [] as IAppstoreApp[], - categories: [] as IAppstoreCategory[], - updateCount: loadState('settings', 'appstoreUpdateCount', 0), - loading: { - apps: false, - categories: false, - }, - loadingList: false, - gettingCategoriesPromise: null, - }), - - actions: { - async loadCategories(force = false) { - if (this.categories.length > 0 && !force) { - return - } - - try { - this.loading.categories = true - const { data: categories } = await axios.get(generateUrl('settings/apps/categories')) - - for (const category of categories) { - category.icon = APPSTORE_CATEGORY_ICONS[category.id] ?? '' - } - - this.$patch({ - 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 { data } = await axios.get<{ apps: IAppstoreApp[] }>(generateUrl('settings/apps/list')) - - this.$patch({ - apps: data.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 - }, - - getAppById(appId: string): IAppstoreApp|null { - return this.apps.find(({ id }) => id === appId) ?? null - }, - }, -}) diff --git a/apps/settings/src/store/apps.js b/apps/settings/src/store/apps.js deleted file mode 100644 index ed5a7245371..00000000000 --- a/apps/settings/src/store/apps.js +++ /dev/null @@ -1,343 +0,0 @@ -/** - * 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.') + '
' + 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 } diff --git a/apps/settings/src/store/index.js b/apps/settings/src/store/index.js index 910185edb51..032140cb2b7 100644 --- a/apps/settings/src/store/index.js +++ b/apps/settings/src/store/index.js @@ -6,7 +6,6 @@ import Vue from 'vue' import Vuex, { Store } from 'vuex' import users from './users.js' -import apps from './apps.js' import settings from './users-settings.js' import oc from './oc.js' import { showError } from '@nextcloud/dialogs' @@ -34,7 +33,6 @@ export const useStore = () => { store = new Store({ modules: { users, - apps, settings, oc, }, -- 2.39.5