Merge pull request #18233 from nextcloud/enhancement/recommended-apps-page
Add a dedicated page for the recommended apps installationpull/18364/head
commit
a33a4c53ef
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,53 @@ |
||||
<?php declare(strict_types=1); |
||||
|
||||
/** |
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> |
||||
* |
||||
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* 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/>. |
||||
*/ |
||||
|
||||
namespace OC\Core\Controller; |
||||
|
||||
use OCP\AppFramework\Controller; |
||||
use OCP\AppFramework\Http\ContentSecurityPolicy; |
||||
use OCP\AppFramework\Http\Response; |
||||
use OCP\AppFramework\Http\StandaloneTemplateResponse; |
||||
use OCP\IInitialStateService; |
||||
use OCP\IRequest; |
||||
|
||||
class RecommendedAppsController extends Controller { |
||||
|
||||
/** @var IInitialStateService */ |
||||
private $initialStateService; |
||||
|
||||
public function __construct(IRequest $request, |
||||
IInitialStateService $initialStateService) { |
||||
parent::__construct('core', $request); |
||||
$this->initialStateService = $initialStateService; |
||||
} |
||||
|
||||
/** |
||||
* @NoCSRFRequired |
||||
* @return Response |
||||
*/ |
||||
public function index(): Response { |
||||
$this->initialStateService->provideInitialState('core', 'defaultPageUrl', \OC_Util::getDefaultPageUrl()); |
||||
return new StandaloneTemplateResponse($this->appName, 'recommendedapps', [], 'guest'); |
||||
} |
||||
|
||||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,194 @@ |
||||
<!-- |
||||
- @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> |
||||
- |
||||
- @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> |
||||
- |
||||
- @license GNU AGPL version 3 or any later version |
||||
- |
||||
- 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/>. |
||||
--> |
||||
|
||||
<template> |
||||
<div class="update"> |
||||
<h2>{{ t('core', 'Recommended apps') }}</h2> |
||||
<p v-if="loadingApps" class="loading"> |
||||
{{ t('core', 'Loading apps …') }} |
||||
</p> |
||||
<p v-else-if="loadingAppsError" class="loading-error"> |
||||
{{ t('core', 'Could not fetch list of apps from the app store.') }} |
||||
</p> |
||||
<p v-else> |
||||
{{ t('core', 'Installing recommended apps …') }} |
||||
</p> |
||||
<div v-for="app in recommendedApps" :key="app.id" class="app"> |
||||
<img :src="customIcon(app.id)" :alt="t('core', 'Nextcloud app {app}', { app: app.name })"> |
||||
<div class="info"> |
||||
<h3> |
||||
{{ app.name }} |
||||
<span v-if="app.loading" class="icon icon-loading-small" /> |
||||
<span v-else-if="app.active" class="icon icon-checkmark-white" /> |
||||
</h3> |
||||
<p v-html="customDescription(app.id)" /> |
||||
<p v-if="app.installationError" class="error"> |
||||
{{ t('core', 'App download or installation failed') }} |
||||
</p> |
||||
<p v-else-if="!app.isCompatible" class="error"> |
||||
{{ t('core', 'Can\'t install this app because it is not compatible') }} |
||||
</p> |
||||
<p v-else-if="!app.canInstall" class="error"> |
||||
{{ t('core', 'Can\'t install this app') }} |
||||
</p> |
||||
</div> |
||||
</div> |
||||
<a :href="defaultPageUrl">{{ t('core', 'Go back') }}</a> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import axios from '@nextcloud/axios' |
||||
import { generateUrl, imagePath } from '@nextcloud/router' |
||||
import { loadState } from '@nextcloud/initial-state' |
||||
import pLimit from 'p-limit' |
||||
import { translate as t } from '@nextcloud/l10n' |
||||
|
||||
import logger from '../../logger' |
||||
|
||||
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') |
||||
}, |
||||
talk: { |
||||
description: t('core', 'Screensharing, online meetings and web conferencing – on desktop and with mobile apps.') |
||||
} |
||||
} |
||||
const recommendedIds = Object.keys(recommended) |
||||
const defaultPageUrl = loadState('core', 'defaultPageUrl') |
||||
|
||||
export default { |
||||
name: 'RecommendedApps', |
||||
data() { |
||||
return { |
||||
loadingApps: true, |
||||
loadingAppsError: false, |
||||
apps: [], |
||||
defaultPageUrl |
||||
} |
||||
}, |
||||
computed: { |
||||
recommendedApps() { |
||||
return this.apps.filter(app => recommendedIds.includes(app.id)) |
||||
} |
||||
}, |
||||
mounted() { |
||||
return axios.get(generateUrl('settings/apps/list')) |
||||
.then(resp => resp.data) |
||||
.then(data => { |
||||
logger.info(`${data.apps.length} apps fetched`) |
||||
|
||||
this.apps = data.apps.map(app => Object.assign(app, { loading: false, installationError: false })) |
||||
logger.debug(`${this.recommendedApps.length} recommended apps found`, { apps: this.recommendedApps }) |
||||
|
||||
this.installApps() |
||||
}) |
||||
.catch(error => { |
||||
logger.error('could not fetch app list', { error }) |
||||
|
||||
this.loadingAppsError = true |
||||
}) |
||||
.then(() => { |
||||
this.loadingApps = false |
||||
}) |
||||
}, |
||||
methods: { |
||||
installApps() { |
||||
const limit = pLimit(1) |
||||
const installing = this.recommendedApps |
||||
.filter(app => !app.active && app.isCompatible && app.canInstall) |
||||
.map(app => limit(() => { |
||||
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.installationError = true |
||||
}) |
||||
.then(() => { |
||||
logger.info(`installed ${app.id}`) |
||||
app.loading = false |
||||
}) |
||||
})) |
||||
logger.debug(`installing ${installing.length} recommended apps`) |
||||
Promise.all(installing) |
||||
.then(() => { |
||||
logger.info('all recommended apps installed, redirecting …') |
||||
|
||||
window.location = defaultPageUrl |
||||
}) |
||||
.catch(error => logger.error('could not install recommended apps', { error })) |
||||
}, |
||||
customIcon(appId) { |
||||
if (!(appId in recommended)) { |
||||
logger.warn(`no app icon for recommended app ${appId}`) |
||||
return imagePath('core', 'places/default-app-icon.svg') |
||||
} |
||||
return recommended[appId].icon |
||||
}, |
||||
customDescription(appId) { |
||||
if (!(appId in recommended)) { |
||||
logger.warn(`no app description for recommended app ${appId}`) |
||||
return '' |
||||
} |
||||
return recommended[appId].description |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
p.loading, p.loading-error { |
||||
height: 100px; |
||||
} |
||||
.app { |
||||
display: flex; |
||||
flex-direction: row; |
||||
|
||||
img { |
||||
height: 64px; |
||||
width: 64px; |
||||
} |
||||
|
||||
img, .info { |
||||
padding: 12px; |
||||
} |
||||
|
||||
.info { |
||||
h3 { |
||||
text-align: left; |
||||
} |
||||
|
||||
h3 > span.icon { |
||||
display: inline-block; |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,37 @@ |
||||
/* |
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> |
||||
* |
||||
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* 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 { getCurrentUser } from '@nextcloud/auth' |
||||
import { getLoggerBuilder } from '@nextcloud/logger' |
||||
|
||||
const getLogger = user => { |
||||
if (user === null) { |
||||
return getLoggerBuilder() |
||||
.setApp('core') |
||||
.build() |
||||
} |
||||
return getLoggerBuilder() |
||||
.setApp('core') |
||||
.setUid(user.uid) |
||||
.build() |
||||
} |
||||
|
||||
export default getLogger(getCurrentUser()) |
@ -0,0 +1,44 @@ |
||||
/* |
||||
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> |
||||
* |
||||
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* 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 { getRequestToken } from '@nextcloud/auth' |
||||
import { generateFilePath } from '@nextcloud/router' |
||||
import { translate as t } from '@nextcloud/l10n' |
||||
import Vue from 'vue' |
||||
|
||||
import logger from './logger' |
||||
import RecommendedApps from './components/setup/RecommendedApps' |
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(getRequestToken()) |
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_public_path__ = generateFilePath('core', '', 'js/') |
||||
|
||||
Vue.mixin({ |
||||
methods: { |
||||
t |
||||
} |
||||
}) |
||||
|
||||
const View = Vue.extend(RecommendedApps) |
||||
new View().$mount('#recommended-apps') |
||||
|
||||
logger.debug('recommended apps view rendered') |
Loading…
Reference in new issue