diff options
Diffstat (limited to 'core/src/components/setup/RecommendedApps.vue')
-rw-r--r-- | core/src/components/setup/RecommendedApps.vue | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/core/src/components/setup/RecommendedApps.vue b/core/src/components/setup/RecommendedApps.vue new file mode 100644 index 00000000000..f2120c28402 --- /dev/null +++ b/core/src/components/setup/RecommendedApps.vue @@ -0,0 +1,262 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="guest-box" data-cy-setup-recommended-apps> + <h2>{{ t('core', 'Recommended apps') }}</h2> + <p v-if="loadingApps" class="loading text-center"> + {{ t('core', 'Loading apps …') }} + </p> + <p v-else-if="loadingAppsError" class="loading-error text-center"> + {{ t('core', 'Could not fetch list of apps from the App Store.') }} + </p> + + <div v-for="app in recommendedApps" :key="app.id" class="app"> + <template v-if="!isHidden(app.id)"> + <img :src="customIcon(app.id)" alt=""> + <div class="info"> + <h3>{{ customName(app) }}</h3> + <p v-text="customDescription(app.id)" /> + <p v-if="app.installationError"> + <strong>{{ t('core', 'App download or installation failed') }}</strong> + </p> + <p v-else-if="!app.isCompatible"> + <strong>{{ t('core', 'Cannot install this app because it is not compatible') }}</strong> + </p> + <p v-else-if="!app.canInstall"> + <strong>{{ t('core', 'Cannot install this app') }}</strong> + </p> + </div> + <NcCheckboxRadioSwitch :checked="app.isSelected || app.active" + :disabled="!app.isCompatible || app.active" + :loading="app.loading" + @update:checked="toggleSelect(app.id)" /> + </template> + </div> + + <div class="dialog-row"> + <NcButton v-if="showInstallButton && !installingApps" + data-cy-setup-recommended-apps-skip + :href="defaultPageUrl" + variant="tertiary"> + {{ t('core', 'Skip') }} + </NcButton> + + <NcButton v-if="showInstallButton" + data-cy-setup-recommended-apps-install + :disabled="installingApps || !isAnyAppSelected" + variant="primary" + @click.stop.prevent="installApps"> + {{ installingApps ? t('core', 'Installing apps …') : t('core', 'Install recommended apps') }} + </NcButton> + </div> + </div> +</template> + +<script> +import { t } from '@nextcloud/l10n' +import { loadState } from '@nextcloud/initial-state' +import { generateUrl, imagePath } from '@nextcloud/router' +import axios from '@nextcloud/axios' +import pLimit from 'p-limit' +import logger from '../../logger.js' + +import NcButton from '@nextcloud/vue/components/NcButton' +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' + +const recommended = { + calendar: { + description: t('core', 'Schedule work & meetings, synced with all your devices.'), + icon: imagePath('core', 'places/calendar.svg'), + }, + contacts: { + description: t('core', 'Keep your colleagues and friends in one place without leaking their private info.'), + icon: imagePath('core', 'places/contacts.svg'), + }, + mail: { + description: t('core', 'Simple email app nicely integrated with Files, Contacts and Calendar.'), + icon: imagePath('core', 'actions/mail.svg'), + }, + spreed: { + description: t('core', 'Chatting, video calls, screen sharing, online meetings and web conferencing – in your browser and with mobile apps.'), + icon: imagePath('core', 'apps/spreed.svg'), + }, + richdocuments: { + name: 'Nextcloud Office', + description: t('core', 'Collaborative documents, spreadsheets and presentations, built on Collabora Online.'), + icon: imagePath('core', 'apps/richdocuments.svg'), + }, + notes: { + description: t('core', 'Distraction free note taking app.'), + icon: imagePath('core', 'apps/notes.svg'), + }, + richdocumentscode: { + hidden: true, + }, +} +const recommendedIds = Object.keys(recommended) + +export default { + name: 'RecommendedApps', + components: { + NcCheckboxRadioSwitch, + NcButton, + }, + data() { + return { + showInstallButton: false, + installingApps: false, + loadingApps: true, + loadingAppsError: false, + apps: [], + defaultPageUrl: loadState('core', 'defaultPageUrl'), + } + }, + computed: { + recommendedApps() { + return this.apps.filter(app => recommendedIds.includes(app.id)) + }, + isAnyAppSelected() { + return this.recommendedApps.some(app => app.isSelected) + }, + }, + async mounted() { + try { + const { data } = await axios.get(generateUrl('settings/apps/list')) + logger.info(`${data.apps.length} apps fetched`) + + this.apps = data.apps.map(app => Object.assign(app, { loading: false, installationError: false, isSelected: app.isCompatible })) + logger.debug(`${this.recommendedApps.length} recommended apps found`, { apps: this.recommendedApps }) + + this.showInstallButton = true + } catch (error) { + logger.error('could not fetch app list', { error }) + + this.loadingAppsError = true + } finally { + this.loadingApps = false + } + }, + methods: { + installApps() { + this.installingApps = true + + const limit = pLimit(1) + const installing = this.recommendedApps + .filter(app => !app.active && app.isCompatible && app.canInstall && app.isSelected) + .map(app => limit(async () => { + logger.info(`installing ${app.id}`) + app.loading = true + return axios.post(generateUrl('settings/apps/enable'), { appIds: [app.id], groups: [] }) + .catch(error => { + logger.error(`could not install ${app.id}`, { error }) + app.isSelected = false + app.installationError = true + }) + .then(() => { + logger.info(`installed ${app.id}`) + app.loading = false + app.active = true + }) + })) + logger.debug(`installing ${installing.length} recommended apps`) + Promise.all(installing) + .then(() => { + logger.info('all recommended apps installed, redirecting …') + + window.location = this.defaultPageUrl + }) + .catch(error => logger.error('could not install recommended apps', { error })) + }, + customIcon(appId) { + if (!(appId in recommended) || !recommended[appId].icon) { + logger.warn(`no app icon for recommended app ${appId}`) + return imagePath('core', 'places/default-app-icon.svg') + } + return recommended[appId].icon + }, + customName(app) { + if (!(app.id in recommended)) { + return app.name + } + return recommended[app.id].name || app.name + }, + customDescription(appId) { + if (!(appId in recommended)) { + logger.warn(`no app description for recommended app ${appId}`) + return '' + } + return recommended[appId].description + }, + isHidden(appId) { + if (!(appId in recommended)) { + return false + } + return !!recommended[appId].hidden + }, + toggleSelect(appId) { + // disable toggle when installButton is disabled + if (!(appId in recommended) || !this.showInstallButton) { + return + } + const index = this.apps.findIndex(app => app.id === appId) + this.$set(this.apps[index], 'isSelected', !this.apps[index].isSelected) + }, + }, +} +</script> + +<style lang="scss" scoped> +.dialog-row { + display: flex; + justify-content: end; + margin-top: 8px; +} + +p { + &.loading, + &.loading-error { + height: 100px; + } + + &:last-child { + margin-top: 10px; + } +} + +.text-center { + text-align: center; +} + +.app { + display: flex; + flex-direction: row; + + img { + height: 50px; + width: 50px; + filter: var(--background-invert-if-dark); + } + + img, .info { + padding: 12px; + } + + .info { + h3, p { + text-align: start; + } + + h3 { + margin-top: 0; + } + } + + .checkbox-radio-switch { + margin-inline-start: auto; + padding: 0 2px; + } +} +</style> |