Merge pull request #9565 from nextcloud/feature/noid/app-settings
Migrate apps management to Vue.js
This commit is contained in:
commit
b49f8e43bd
|
@ -323,6 +323,7 @@ class AppManager implements IAppManager {
|
|||
public function clearAppsCache() {
|
||||
$settingsMemCache = $this->memCacheFactory->createDistributed('settings');
|
||||
$settingsMemCache->clear('listApps');
|
||||
$this->appInfos = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -887,6 +887,7 @@ class OC_App {
|
|||
}
|
||||
self::registerAutoloading($appId, $appPath);
|
||||
|
||||
\OC::$server->getAppManager()->clearAppsCache();
|
||||
$appData = self::getAppInfo($appId);
|
||||
self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
|
||||
|
||||
|
@ -900,6 +901,7 @@ class OC_App {
|
|||
self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
|
||||
self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
|
||||
// update appversion in app manager
|
||||
\OC::$server->getAppManager()->clearAppsCache();
|
||||
\OC::$server->getAppManager()->getAppVersion($appId, false);
|
||||
|
||||
// run upgrade code
|
||||
|
|
|
@ -100,6 +100,7 @@ interface IAppManager {
|
|||
*
|
||||
* @param string $appId
|
||||
* @param \OCP\IGroup[] $groups
|
||||
* @throws \Exception
|
||||
* @since 8.0.0
|
||||
*/
|
||||
public function enableAppForGroups($appId, $groups);
|
||||
|
|
|
@ -38,11 +38,14 @@ use OC\App\AppStore\Version\VersionParser;
|
|||
use OC\App\DependencyAnalyzer;
|
||||
use OC\App\Platform;
|
||||
use OC\Installer;
|
||||
use OC_App;
|
||||
use OCP\App\IAppManager;
|
||||
use \OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\ILogger;
|
||||
use OCP\INavigationManager;
|
||||
use OCP\IRequest;
|
||||
use OCP\IL10N;
|
||||
|
@ -54,11 +57,6 @@ use OCP\L10N\IFactory;
|
|||
* @package OC\Settings\Controller
|
||||
*/
|
||||
class AppSettingsController extends Controller {
|
||||
const CAT_ENABLED = 0;
|
||||
const CAT_DISABLED = 1;
|
||||
const CAT_ALL_INSTALLED = 2;
|
||||
const CAT_APP_BUNDLES = 3;
|
||||
const CAT_UPDATES = 4;
|
||||
|
||||
/** @var \OCP\IL10N */
|
||||
private $l10n;
|
||||
|
@ -80,6 +78,11 @@ class AppSettingsController extends Controller {
|
|||
private $installer;
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
/** @var ILogger */
|
||||
private $logger;
|
||||
|
||||
/** @var array */
|
||||
private $allApps = [];
|
||||
|
||||
/**
|
||||
* @param string $appName
|
||||
|
@ -94,6 +97,7 @@ class AppSettingsController extends Controller {
|
|||
* @param BundleFetcher $bundleFetcher
|
||||
* @param Installer $installer
|
||||
* @param IURLGenerator $urlGenerator
|
||||
* @param ILogger $logger
|
||||
*/
|
||||
public function __construct(string $appName,
|
||||
IRequest $request,
|
||||
|
@ -106,7 +110,8 @@ class AppSettingsController extends Controller {
|
|||
IFactory $l10nFactory,
|
||||
BundleFetcher $bundleFetcher,
|
||||
Installer $installer,
|
||||
IURLGenerator $urlGenerator) {
|
||||
IURLGenerator $urlGenerator,
|
||||
ILogger $logger) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->l10n = $l10n;
|
||||
$this->config = $config;
|
||||
|
@ -118,26 +123,25 @@ class AppSettingsController extends Controller {
|
|||
$this->bundleFetcher = $bundleFetcher;
|
||||
$this->installer = $installer;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* @param string $category
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
public function viewApps($category = '') {
|
||||
if ($category === '') {
|
||||
$category = 'installed';
|
||||
}
|
||||
|
||||
public function viewApps(): TemplateResponse {
|
||||
\OC_Util::addScript('settings', 'apps');
|
||||
\OC_Util::addVendorScript('core', 'marked/marked.min');
|
||||
$params = [];
|
||||
$params['category'] = $category;
|
||||
$params['appstoreEnabled'] = $this->config->getSystemValue('appstoreenabled', true) === true;
|
||||
$params['urlGenerator'] = $this->urlGenerator;
|
||||
$params['updateCount'] = count($this->getAppsWithUpdates());
|
||||
$params['developerDocumentation'] = $this->urlGenerator->linkToDocs('developer-manual');
|
||||
$params['bundles'] = $this->getBundles();
|
||||
$this->navigationManager->setActiveEntry('core_apps');
|
||||
|
||||
$templateResponse = new TemplateResponse($this->appName, 'apps', $params, 'user');
|
||||
$templateResponse = new TemplateResponse('settings', 'settings', ['serverData' => $params]);
|
||||
$policy = new ContentSecurityPolicy();
|
||||
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
|
||||
$templateResponse->setContentSecurityPolicy($policy);
|
||||
|
@ -145,17 +149,45 @@ class AppSettingsController extends Controller {
|
|||
return $templateResponse;
|
||||
}
|
||||
|
||||
private function getAppsWithUpdates() {
|
||||
$appClass = new \OC_App();
|
||||
$apps = $appClass->listAllApps();
|
||||
foreach($apps as $key => $app) {
|
||||
$newVersion = $this->installer->isUpdateAvailable($app['id']);
|
||||
if($newVersion === false) {
|
||||
unset($apps[$key]);
|
||||
}
|
||||
}
|
||||
return $apps;
|
||||
}
|
||||
|
||||
private function getBundles() {
|
||||
$result = [];
|
||||
$bundles = $this->bundleFetcher->getBundles();
|
||||
foreach ($bundles as $bundle) {
|
||||
$result[] = [
|
||||
'name' => $bundle->getName(),
|
||||
'id' => $bundle->getIdentifier(),
|
||||
'appIdentifiers' => $bundle->getAppIdentifiers()
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available categories
|
||||
*
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function listCategories(): JSONResponse {
|
||||
return new JSONResponse($this->getAllCategories());
|
||||
}
|
||||
|
||||
private function getAllCategories() {
|
||||
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
|
||||
|
||||
$updateCount = count($this->getAppsWithUpdates());
|
||||
$formattedCategories = [
|
||||
['id' => self::CAT_ALL_INSTALLED, 'ident' => 'installed', 'displayName' => (string)$this->l10n->t('Your apps')],
|
||||
['id' => self::CAT_UPDATES, 'ident' => 'updates', 'displayName' => (string)$this->l10n->t('Updates'), 'counter' => $updateCount],
|
||||
['id' => self::CAT_ENABLED, 'ident' => 'enabled', 'displayName' => (string)$this->l10n->t('Enabled apps')],
|
||||
['id' => self::CAT_DISABLED, 'ident' => 'disabled', 'displayName' => (string)$this->l10n->t('Disabled apps')],
|
||||
['id' => self::CAT_APP_BUNDLES, 'ident' => 'app-bundles', 'displayName' => (string)$this->l10n->t('App bundles')],
|
||||
];
|
||||
$formattedCategories = [];
|
||||
$categories = $this->categoryFetcher->get();
|
||||
foreach($categories as $category) {
|
||||
$formattedCategories[] = [
|
||||
|
@ -168,31 +200,108 @@ class AppSettingsController extends Controller {
|
|||
return $formattedCategories;
|
||||
}
|
||||
|
||||
private function fetchApps() {
|
||||
$appClass = new \OC_App();
|
||||
$apps = $appClass->listAllApps();
|
||||
foreach ($apps as $app) {
|
||||
$app['installed'] = true;
|
||||
$this->allApps[$app['id']] = $app;
|
||||
}
|
||||
|
||||
$apps = $this->getAppsForCategory('');
|
||||
foreach ($apps as $app) {
|
||||
$app['appstore'] = true;
|
||||
if (!array_key_exists($app['id'], $this->allApps)) {
|
||||
$this->allApps[$app['id']] = $app;
|
||||
} else {
|
||||
$this->allApps[$app['id']] = array_merge($this->allApps[$app['id']], $app);
|
||||
}
|
||||
}
|
||||
|
||||
// add bundle information
|
||||
$bundles = $this->bundleFetcher->getBundles();
|
||||
foreach($bundles as $bundle) {
|
||||
foreach($bundle->getAppIdentifiers() as $identifier) {
|
||||
foreach($this->allApps as &$app) {
|
||||
if($app['id'] === $identifier) {
|
||||
$app['bundleId'] = $bundle->getIdentifier();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getAllApps() {
|
||||
return $this->allApps;
|
||||
}
|
||||
/**
|
||||
* Get all available categories
|
||||
* Get all available apps in a category
|
||||
*
|
||||
* @param string $category
|
||||
* @return JSONResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function listCategories() {
|
||||
return new JSONResponse($this->getAllCategories());
|
||||
public function listApps(): JSONResponse {
|
||||
|
||||
$this->fetchApps();
|
||||
$apps = $this->getAllApps();
|
||||
|
||||
$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n);
|
||||
|
||||
// Extend existing app details
|
||||
$apps = array_map(function($appData) use ($dependencyAnalyzer) {
|
||||
$appstoreData = $appData['appstoreData'];
|
||||
$appData['screenshot'] = isset($appstoreData['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($appstoreData['screenshots'][0]['url']) : '';
|
||||
|
||||
$newVersion = $this->installer->isUpdateAvailable($appData['id']);
|
||||
if($newVersion && $this->appManager->isInstalled($appData['id'])) {
|
||||
$appData['update'] = $newVersion;
|
||||
}
|
||||
|
||||
// fix groups to be an array
|
||||
$groups = array();
|
||||
if (is_string($appData['groups'])) {
|
||||
$groups = json_decode($appData['groups']);
|
||||
}
|
||||
$appData['groups'] = $groups;
|
||||
$appData['canUnInstall'] = !$appData['active'] && $appData['removable'];
|
||||
|
||||
// fix licence vs license
|
||||
if (isset($appData['license']) && !isset($appData['licence'])) {
|
||||
$appData['licence'] = $appData['license'];
|
||||
}
|
||||
|
||||
// analyse dependencies
|
||||
$missing = $dependencyAnalyzer->analyze($appData);
|
||||
$appData['canInstall'] = empty($missing);
|
||||
$appData['missingDependencies'] = $missing;
|
||||
|
||||
$appData['missingMinOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
|
||||
$appData['missingMaxOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
|
||||
|
||||
return $appData;
|
||||
}, $apps);
|
||||
|
||||
usort($apps, [$this, 'sortApps']);
|
||||
|
||||
return new JSONResponse(['apps' => $apps, 'status' => 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all apps for a category
|
||||
* Get all apps for a category from the app store
|
||||
*
|
||||
* @param string $requestedCategory
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getAppsForCategory($requestedCategory) {
|
||||
private function getAppsForCategory($requestedCategory = ''): array {
|
||||
$versionParser = new VersionParser();
|
||||
$formattedApps = [];
|
||||
$apps = $this->appFetcher->get();
|
||||
foreach($apps as $app) {
|
||||
if (isset($app['isFeatured'])) {
|
||||
$app['featured'] = $app['isFeatured'];
|
||||
}
|
||||
|
||||
// Skip all apps not in the requested category
|
||||
if ($requestedCategory !== '') {
|
||||
$isInCategory = false;
|
||||
foreach($app['categories'] as $category) {
|
||||
if($category === $requestedCategory) {
|
||||
|
@ -202,6 +311,7 @@ class AppSettingsController extends Controller {
|
|||
if(!$isInCategory) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']);
|
||||
$nextCloudVersionDependencies = [];
|
||||
|
@ -240,7 +350,7 @@ class AppSettingsController extends Controller {
|
|||
|
||||
$currentVersion = '';
|
||||
if($this->appManager->isInstalled($app['id'])) {
|
||||
$currentVersion = \OC_App::getAppVersion($app['id']);
|
||||
$currentVersion = $this->appManager->getAppVersion($app['id']);
|
||||
} else {
|
||||
$currentLanguage = $app['releases'][0]['version'];
|
||||
}
|
||||
|
@ -249,6 +359,7 @@ class AppSettingsController extends Controller {
|
|||
'id' => $app['id'],
|
||||
'name' => isset($app['translations'][$currentLanguage]['name']) ? $app['translations'][$currentLanguage]['name'] : $app['translations']['en']['name'],
|
||||
'description' => isset($app['translations'][$currentLanguage]['description']) ? $app['translations'][$currentLanguage]['description'] : $app['translations']['en']['description'],
|
||||
'summary' => isset($app['translations'][$currentLanguage]['summary']) ? $app['translations'][$currentLanguage]['summary'] : $app['translations']['en']['summary'],
|
||||
'license' => $app['releases'][0]['licenses'],
|
||||
'author' => $authors,
|
||||
'shipped' => false,
|
||||
|
@ -267,208 +378,168 @@ class AppSettingsController extends Controller {
|
|||
$nextCloudVersionDependencies,
|
||||
$phpDependencies
|
||||
),
|
||||
'level' => ($app['featured'] === true) ? 200 : 100,
|
||||
'level' => ($app['isFeatured'] === true) ? 200 : 100,
|
||||
'missingMaxOwnCloudVersion' => false,
|
||||
'missingMinOwnCloudVersion' => false,
|
||||
'canInstall' => true,
|
||||
'preview' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '',
|
||||
'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '',
|
||||
'score' => $app['ratingOverall'],
|
||||
'ratingNumOverall' => $app['ratingNumOverall'],
|
||||
'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5 ? true : false,
|
||||
'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5,
|
||||
'removable' => $existsLocally,
|
||||
'active' => $this->appManager->isEnabledForUser($app['id']),
|
||||
'needsDownload' => !$existsLocally,
|
||||
'groups' => $groups,
|
||||
'fromAppStore' => true,
|
||||
'appstoreData' => $app,
|
||||
];
|
||||
|
||||
|
||||
$newVersion = $this->installer->isUpdateAvailable($app['id']);
|
||||
if($newVersion && $this->appManager->isInstalled($app['id'])) {
|
||||
$formattedApps[count($formattedApps)-1]['update'] = $newVersion;
|
||||
}
|
||||
}
|
||||
|
||||
return $formattedApps;
|
||||
}
|
||||
|
||||
private function getAppsWithUpdates() {
|
||||
$appClass = new \OC_App();
|
||||
$apps = $appClass->listAllApps();
|
||||
foreach($apps as $key => $app) {
|
||||
$newVersion = $this->installer->isUpdateAvailable($app['id']);
|
||||
if($newVersion !== false) {
|
||||
$apps[$key]['update'] = $newVersion;
|
||||
} else {
|
||||
unset($apps[$key]);
|
||||
}
|
||||
}
|
||||
usort($apps, function ($a, $b) {
|
||||
$a = (string)$a['name'];
|
||||
$b = (string)$b['name'];
|
||||
if ($a === $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($a < $b) ? -1 : 1;
|
||||
});
|
||||
return $apps;
|
||||
/**
|
||||
* @PasswordConfirmationRequired
|
||||
*
|
||||
* @param string $appId
|
||||
* @param array $groups
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function enableApp(string $appId, array $groups = []): JSONResponse {
|
||||
return $this->enableApps([$appId], $groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available apps in a category
|
||||
* Enable one or more apps
|
||||
*
|
||||
* @param string $category
|
||||
* apps will be enabled for specific groups only if $groups is defined
|
||||
*
|
||||
* @PasswordConfirmationRequired
|
||||
* @param array $appIds
|
||||
* @param array $groups
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function listApps($category = '') {
|
||||
$appClass = new \OC_App();
|
||||
public function enableApps(array $appIds, array $groups = []): JSONResponse {
|
||||
try {
|
||||
$updateRequired = false;
|
||||
|
||||
switch ($category) {
|
||||
// installed apps
|
||||
case 'installed':
|
||||
$apps = $appClass->listAllApps();
|
||||
foreach ($appIds as $appId) {
|
||||
$appId = OC_App::cleanAppId($appId);
|
||||
|
||||
foreach($apps as $key => $app) {
|
||||
$newVersion = $this->installer->isUpdateAvailable($app['id']);
|
||||
$apps[$key]['update'] = $newVersion;
|
||||
// Check if app is already downloaded
|
||||
/** @var Installer $installer */
|
||||
$installer = \OC::$server->query(Installer::class);
|
||||
$isDownloaded = $installer->isDownloaded($appId);
|
||||
|
||||
if(!$isDownloaded) {
|
||||
$installer->downloadApp($appId);
|
||||
}
|
||||
|
||||
usort($apps, function ($a, $b) {
|
||||
$installer->installApp($appId);
|
||||
|
||||
if (count($groups) > 0) {
|
||||
$this->appManager->enableAppForGroups($appId, $this->getGroupList($groups));
|
||||
} else {
|
||||
$this->appManager->enableApp($appId);
|
||||
}
|
||||
if (\OC_App::shouldUpgrade($appId)) {
|
||||
$updateRequired = true;
|
||||
}
|
||||
}
|
||||
return new JSONResponse(['data' => ['update_required' => $updateRequired]]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->logException($e);
|
||||
return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private function getGroupList(array $groups) {
|
||||
$groupManager = \OC::$server->getGroupManager();
|
||||
$groupsList = [];
|
||||
foreach ($groups as $group) {
|
||||
$groupItem = $groupManager->get($group);
|
||||
if ($groupItem instanceof \OCP\IGroup) {
|
||||
$groupsList[] = $groupManager->get($group);
|
||||
}
|
||||
}
|
||||
return $groupsList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @PasswordConfirmationRequired
|
||||
*
|
||||
* @param string $appId
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function disableApp(string $appId): JSONResponse {
|
||||
return $this->disableApps([$appId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @PasswordConfirmationRequired
|
||||
*
|
||||
* @param array $appIds
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function disableApps(array $appIds): JSONResponse {
|
||||
try {
|
||||
foreach ($appIds as $appId) {
|
||||
$appId = OC_App::cleanAppId($appId);
|
||||
$this->appManager->disableApp($appId);
|
||||
}
|
||||
return new JSONResponse([]);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->logException($e);
|
||||
return new JSONResponse(['data' => ['message' => $e->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @PasswordConfirmationRequired
|
||||
*
|
||||
* @param string $appId
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function uninstallApp(string $appId): JSONResponse {
|
||||
$appId = OC_App::cleanAppId($appId);
|
||||
$result = $this->installer->removeApp($appId);
|
||||
if($result !== false) {
|
||||
$this->appManager->clearAppsCache();
|
||||
return new JSONResponse(['data' => ['appid' => $appId]]);
|
||||
}
|
||||
return new JSONResponse(['data' => ['message' => $this->l10n->t('Couldn\'t remove app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $appId
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function updateApp(string $appId): JSONResponse {
|
||||
$appId = OC_App::cleanAppId($appId);
|
||||
|
||||
$this->config->setSystemValue('maintenance', true);
|
||||
try {
|
||||
$result = $this->installer->updateAppstoreApp($appId);
|
||||
$this->config->setSystemValue('maintenance', false);
|
||||
} catch (\Exception $ex) {
|
||||
$this->config->setSystemValue('maintenance', false);
|
||||
return new JSONResponse(['data' => ['message' => $ex->getMessage()]], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
if ($result !== false) {
|
||||
return new JSONResponse(['data' => ['appid' => $appId]]);
|
||||
}
|
||||
return new JSONResponse(['data' => ['message' => $this->l10n->t('Couldn\'t update app.')]], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
private function sortApps($a, $b) {
|
||||
$a = (string)$a['name'];
|
||||
$b = (string)$b['name'];
|
||||
if ($a === $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($a < $b) ? -1 : 1;
|
||||
});
|
||||
break;
|
||||
// updates
|
||||
case 'updates':
|
||||
$apps = $this->getAppsWithUpdates();
|
||||
break;
|
||||
// enabled apps
|
||||
case 'enabled':
|
||||
$apps = $appClass->listAllApps();
|
||||
$apps = array_filter($apps, function ($app) {
|
||||
return $app['active'];
|
||||
});
|
||||
|
||||
foreach($apps as $key => $app) {
|
||||
$newVersion = $this->installer->isUpdateAvailable($app['id']);
|
||||
$apps[$key]['update'] = $newVersion;
|
||||
}
|
||||
|
||||
usort($apps, function ($a, $b) {
|
||||
$a = (string)$a['name'];
|
||||
$b = (string)$b['name'];
|
||||
if ($a === $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($a < $b) ? -1 : 1;
|
||||
});
|
||||
break;
|
||||
// disabled apps
|
||||
case 'disabled':
|
||||
$apps = $appClass->listAllApps();
|
||||
$apps = array_filter($apps, function ($app) {
|
||||
return !$app['active'];
|
||||
});
|
||||
|
||||
$apps = array_map(function ($app) {
|
||||
$newVersion = $this->installer->isUpdateAvailable($app['id']);
|
||||
if ($newVersion !== false) {
|
||||
$app['update'] = $newVersion;
|
||||
}
|
||||
return $app;
|
||||
}, $apps);
|
||||
|
||||
usort($apps, function ($a, $b) {
|
||||
$a = (string)$a['name'];
|
||||
$b = (string)$b['name'];
|
||||
if ($a === $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($a < $b) ? -1 : 1;
|
||||
});
|
||||
break;
|
||||
case 'app-bundles':
|
||||
$bundles = $this->bundleFetcher->getBundles();
|
||||
$apps = [];
|
||||
foreach($bundles as $bundle) {
|
||||
$newCategory = true;
|
||||
$allApps = $appClass->listAllApps();
|
||||
$categories = $this->getAllCategories();
|
||||
foreach($categories as $singleCategory) {
|
||||
$newApps = $this->getAppsForCategory($singleCategory['id']);
|
||||
foreach($allApps as $app) {
|
||||
foreach($newApps as $key => $newApp) {
|
||||
if($app['id'] === $newApp['id']) {
|
||||
unset($newApps[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$allApps = array_merge($allApps, $newApps);
|
||||
}
|
||||
|
||||
foreach($bundle->getAppIdentifiers() as $identifier) {
|
||||
foreach($allApps as $app) {
|
||||
if($app['id'] === $identifier) {
|
||||
if($newCategory) {
|
||||
$app['newCategory'] = true;
|
||||
$app['categoryName'] = $bundle->getName();
|
||||
}
|
||||
$app['bundleId'] = $bundle->getIdentifier();
|
||||
$newCategory = false;
|
||||
$apps[] = $app;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$apps = $this->getAppsForCategory($category);
|
||||
|
||||
// sort by score
|
||||
usort($apps, function ($a, $b) {
|
||||
$a = (int)$a['score'];
|
||||
$b = (int)$b['score'];
|
||||
if ($a === $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($a > $b) ? -1 : 1;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// fix groups to be an array
|
||||
$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n);
|
||||
$apps = array_map(function($app) use ($dependencyAnalyzer) {
|
||||
|
||||
// fix groups
|
||||
$groups = array();
|
||||
if (is_string($app['groups'])) {
|
||||
$groups = json_decode($app['groups']);
|
||||
}
|
||||
$app['groups'] = $groups;
|
||||
$app['canUnInstall'] = !$app['active'] && $app['removable'];
|
||||
|
||||
// fix licence vs license
|
||||
if (isset($app['license']) && !isset($app['licence'])) {
|
||||
$app['licence'] = $app['license'];
|
||||
}
|
||||
|
||||
// analyse dependencies
|
||||
$missing = $dependencyAnalyzer->analyze($app);
|
||||
$app['canInstall'] = empty($missing);
|
||||
$app['missingDependencies'] = $missing;
|
||||
|
||||
$app['missingMinOwnCloudVersion'] = !isset($app['dependencies']['nextcloud']['@attributes']['min-version']);
|
||||
$app['missingMaxOwnCloudVersion'] = !isset($app['dependencies']['nextcloud']['@attributes']['max-version']);
|
||||
|
||||
return $app;
|
||||
}, $apps);
|
||||
|
||||
return new JSONResponse(['apps' => $apps, 'status' => 'success']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @author Kamil Domanski <kdomanski@kdemail.net>
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
\OC_JSON::checkAdminUser();
|
||||
\OC_JSON::callCheck();
|
||||
|
||||
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
|
||||
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
|
||||
$l = \OC::$server->getL10N('core');
|
||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!array_key_exists('appid', $_POST)) {
|
||||
OC_JSON::error();
|
||||
exit;
|
||||
}
|
||||
|
||||
$appIds = (array)$_POST['appid'];
|
||||
foreach($appIds as $appId) {
|
||||
$appId = OC_App::cleanAppId($appId);
|
||||
\OC::$server->getAppManager()->disableApp($appId);
|
||||
}
|
||||
OC_JSON::success();
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Bart Visscher <bartv@thisnet.nl>
|
||||
* @author Christopher Schäpers <kondou@ts.unde.re>
|
||||
* @author Kamil Domanski <kdomanski@kdemail.net>
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
use OCP\ILogger;
|
||||
|
||||
OC_JSON::checkAdminUser();
|
||||
\OC_JSON::callCheck();
|
||||
|
||||
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
|
||||
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
|
||||
$l = \OC::$server->getL10N('core');
|
||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
|
||||
exit();
|
||||
}
|
||||
|
||||
$groups = isset($_POST['groups']) ? (array)$_POST['groups'] : [];
|
||||
$appIds = isset($_POST['appIds']) ? (array)$_POST['appIds'] : [];
|
||||
|
||||
try {
|
||||
$updateRequired = false;
|
||||
foreach($appIds as $appId) {
|
||||
$app = new OC_App();
|
||||
$appId = OC_App::cleanAppId($appId);
|
||||
$app->enable($appId, $groups);
|
||||
if(\OC_App::shouldUpgrade($appId)) {
|
||||
$updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
OC_JSON::success(['data' => ['update_required' => $updateRequired]]);
|
||||
} catch (Exception $e) {
|
||||
\OC::$server->getLogger()->logException($e, [
|
||||
'level' => ILogger::DEBUG,
|
||||
'app' => 'core',
|
||||
]);
|
||||
OC_JSON::error(array("data" => array("message" => $e->getMessage()) ));
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
\OC_JSON::checkAdminUser();
|
||||
\OC_JSON::callCheck();
|
||||
|
||||
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
|
||||
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
|
||||
$l = \OC::$server->getL10N('core');
|
||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!array_key_exists('appid', $_POST)) {
|
||||
OC_JSON::error();
|
||||
exit;
|
||||
}
|
||||
|
||||
$appId = (string)$_POST['appid'];
|
||||
$appId = OC_App::cleanAppId($appId);
|
||||
|
||||
// FIXME: move to controller
|
||||
/** @var \OC\Installer $installer */
|
||||
$installer = \OC::$server->query(\OC\Installer::class);
|
||||
$result = $installer->removeApp($app);
|
||||
if($result !== false) {
|
||||
// FIXME: Clear the cache - move that into some sane helper method
|
||||
\OC::$server->getMemCacheFactory()->createDistributed('settings')->remove('listApps-0');
|
||||
\OC::$server->getMemCacheFactory()->createDistributed('settings')->remove('listApps-1');
|
||||
OC_JSON::success(array('data' => array('appid' => $appId)));
|
||||
} else {
|
||||
$l = \OC::$server->getL10N('settings');
|
||||
OC_JSON::error(array('data' => array( 'message' => $l->t("Couldn't remove app.") )));
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Christopher Schäpers <kondou@ts.unde.re>
|
||||
* @author Frank Karlitschek <frank@karlitschek.de>
|
||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
\OC_JSON::checkAdminUser();
|
||||
\OC_JSON::callCheck();
|
||||
|
||||
if (!array_key_exists('appid', $_POST)) {
|
||||
\OC_JSON::error(array(
|
||||
'message' => 'No AppId given!'
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$appId = (string)$_POST['appid'];
|
||||
$appId = OC_App::cleanAppId($appId);
|
||||
|
||||
$config = \OC::$server->getConfig();
|
||||
$config->setSystemValue('maintenance', true);
|
||||
try {
|
||||
$installer = \OC::$server->query(\OC\Installer::class);
|
||||
$result = $installer->updateAppstoreApp($appId);
|
||||
$config->setSystemValue('maintenance', false);
|
||||
} catch(Exception $ex) {
|
||||
$config->setSystemValue('maintenance', false);
|
||||
OC_JSON::error(array('data' => array( 'message' => $ex->getMessage() )));
|
||||
return;
|
||||
}
|
||||
|
||||
if($result !== false) {
|
||||
OC_JSON::success(array('data' => array('appid' => $appId)));
|
||||
} else {
|
||||
$l = \OC::$server->getL10N('settings');
|
||||
OC_JSON::error(array('data' => array( 'message' => $l->t("Couldn't update app.") )));
|
||||
}
|
|
@ -756,38 +756,118 @@ span.version {
|
|||
opacity: .5;
|
||||
}
|
||||
|
||||
@media (min-width: 1601px) {
|
||||
#apps-list .section {
|
||||
width: 22%;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(4n) {
|
||||
margin-right: 20px;
|
||||
.app-settings-content {
|
||||
#searchresults {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#apps-list.store {
|
||||
.section {
|
||||
border: 0;
|
||||
}
|
||||
.app-name {
|
||||
display: block;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.app-name, .app-image * {
|
||||
cursor: pointer;
|
||||
}
|
||||
.app-summary {
|
||||
opacity: .7;
|
||||
}
|
||||
.app-image-icon .icon-settings-dark {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background-size: 45px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.app-score-image {
|
||||
height: 14px;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1201px) and (max-width: 1600px) {
|
||||
#apps-list .section {
|
||||
width: 30%;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(3n) {
|
||||
margin-right: 20px;
|
||||
#app-sidebar #app-details-view {
|
||||
h2 {
|
||||
.icon-settings-dark,
|
||||
svg {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 10px;
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
.app-level {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.app-author, .app-licence {
|
||||
color: $color-text-details;
|
||||
}
|
||||
.app-dependencies {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.app-description p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 14px;
|
||||
opacity: 0.5;
|
||||
z-index: 1;
|
||||
}.
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 901px) and (max-width: 1200px), (min-width: 601px) and (max-width: 770px) {
|
||||
#apps-list .section {
|
||||
width: 40%;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(2n) {
|
||||
margin-right: 20px;
|
||||
@media only screen and (min-width: 1601px) {
|
||||
.store .section {
|
||||
width: 25%;
|
||||
}
|
||||
.with-app-sidebar .store .section {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1600px) {
|
||||
.store .section {
|
||||
width: 25%;
|
||||
}
|
||||
.with-app-sidebar .store .section {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1400px) {
|
||||
.store .section {
|
||||
width: 33%;
|
||||
}
|
||||
.with-app-sidebar .store .section {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
.store .section {
|
||||
width: 50%;
|
||||
}
|
||||
.with-app-sidebar .store .section {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* hide app version and level on narrower screens */
|
||||
@media only screen and (max-width: 768px) {
|
||||
.store .section {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
/* hide app version and level on narrower screens */
|
||||
@media only screen and (max-width: 900px) {
|
||||
#apps-list.installed {
|
||||
.app-version, .app-level {
|
||||
display: none !important;
|
||||
|
@ -795,7 +875,7 @@ span.version {
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
@media only screen and (max-width: 500px) {
|
||||
#apps-list.installed .app-groups {
|
||||
display: none !important;
|
||||
}
|
||||
|
@ -877,30 +957,15 @@ span.version {
|
|||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/* Transition to complete width! */
|
||||
|
||||
.app {
|
||||
&:hover, &:active {
|
||||
max-width: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.appslink {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.appinfo .documentation {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
#apps-list, #apps-list-search {
|
||||
.section {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#apps-list {
|
||||
&.installed {
|
||||
display: table;
|
||||
width: 100%;
|
||||
|
@ -919,6 +984,9 @@ span.version {
|
|||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&.selected {
|
||||
background-color: nc-darken($color-main-background, 3%);
|
||||
}
|
||||
}
|
||||
.groups-enable {
|
||||
margin-top: 0;
|
||||
|
@ -931,11 +999,22 @@ span.version {
|
|||
height: auto;
|
||||
text-align: right;
|
||||
}
|
||||
.app-image-icon svg {
|
||||
.app-image-icon svg,
|
||||
.app-image-icon .icon-settings-dark {
|
||||
margin-top: 5px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: .5;
|
||||
background-size: cover;
|
||||
display: inline-block;
|
||||
}
|
||||
.actions {
|
||||
text-align: right;
|
||||
.icon-loading-small {
|
||||
display: inline-block;
|
||||
top: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.installed) .app-image-icon svg {
|
||||
|
@ -946,8 +1025,6 @@ span.version {
|
|||
height: 64px;
|
||||
opacity: .1;
|
||||
}
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
|
@ -957,10 +1034,7 @@ span.version {
|
|||
.section {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
margin-left: 20px;
|
||||
&.apps-experimental {
|
||||
flex-basis: 90%;
|
||||
}
|
||||
|
||||
h2.app-name {
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
|
@ -1016,10 +1090,6 @@ span.version {
|
|||
}
|
||||
}
|
||||
|
||||
.installed .actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* LOG */
|
||||
#log {
|
||||
white-space: normal;
|
||||
|
|
|
@ -1,610 +1,6 @@
|
|||
/* global Handlebars */
|
||||
|
||||
Handlebars.registerHelper('score', function() {
|
||||
if(this.score) {
|
||||
var score = Math.round( this.score * 10 );
|
||||
var imageName = 'rating/s' + score + '.svg';
|
||||
|
||||
return new Handlebars.SafeString('<img src="' + OC.imagePath('core', imageName) + '">');
|
||||
}
|
||||
return new Handlebars.SafeString('');
|
||||
});
|
||||
Handlebars.registerHelper('level', function() {
|
||||
if(typeof this.level !== 'undefined') {
|
||||
if(this.level === 200) {
|
||||
return new Handlebars.SafeString('<span class="official icon-checkmark">' + t('settings', 'Official') + '</span>');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
OC.Settings = OC.Settings || {};
|
||||
OC.Settings.Apps = OC.Settings.Apps || {
|
||||
markedOptions: {},
|
||||
|
||||
setupGroupsSelect: function($elements) {
|
||||
OC.Settings.setupGroupsSelect($elements, {
|
||||
placeholder: t('core', 'All')
|
||||
});
|
||||
},
|
||||
|
||||
State: {
|
||||
currentCategory: null,
|
||||
currentCategoryElements: null,
|
||||
apps: null,
|
||||
$updateNotification: null,
|
||||
availableUpdates: 0
|
||||
},
|
||||
|
||||
loadCategories: function() {
|
||||
if (this._loadCategoriesCall) {
|
||||
this._loadCategoriesCall.abort();
|
||||
}
|
||||
|
||||
var categories = [
|
||||
{displayName: t('settings', 'Your apps'), ident: 'installed', id: '0'},
|
||||
{displayName: t('settings', 'Enabled apps'), ident: 'enabled', id: '1'},
|
||||
{displayName: t('settings', 'Disabled apps'), ident: 'disabled', id: '2'}
|
||||
];
|
||||
|
||||
var source = $("#categories-template").html();
|
||||
var template = Handlebars.compile(source);
|
||||
var html = template(categories);
|
||||
$('#apps-categories').html(html);
|
||||
|
||||
OC.Settings.Apps.loadCategory($('#app-navigation').attr('data-category'));
|
||||
|
||||
this._loadCategoriesCall = $.ajax(OC.generateUrl('settings/apps/categories'), {
|
||||
data:{},
|
||||
type:'GET',
|
||||
success:function (jsondata) {
|
||||
var html = template(jsondata);
|
||||
var updateCategory = $.grep(jsondata, function(element, index) {
|
||||
return element.ident === 'updates'
|
||||
});
|
||||
$('#apps-categories').html(html);
|
||||
$('#app-category-' + OC.Settings.Apps.State.currentCategory).addClass('active');
|
||||
if (updateCategory.length === 1) {
|
||||
OC.Settings.Apps.State.availableUpdates = updateCategory[0].counter;
|
||||
OC.Settings.Apps.refreshUpdateCounter();
|
||||
}
|
||||
},
|
||||
complete: function() {
|
||||
$('#app-navigation').removeClass('icon-loading');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
loadCategory: function(categoryId) {
|
||||
if (OC.Settings.Apps.State.currentCategory === categoryId) {
|
||||
return;
|
||||
}
|
||||
if (this._loadCategoryCall) {
|
||||
this._loadCategoryCall.abort();
|
||||
}
|
||||
|
||||
$('#app-content').addClass('icon-loading');
|
||||
$('#apps-list')
|
||||
.removeClass('hidden')
|
||||
.html('');
|
||||
$('#apps-list-empty').addClass('hidden');
|
||||
$('#app-category-' + OC.Settings.Apps.State.currentCategory).removeClass('active');
|
||||
$('#app-category-' + categoryId).addClass('active');
|
||||
OC.Settings.Apps.State.currentCategory = categoryId;
|
||||
|
||||
this._loadCategoryCall = $.ajax(OC.generateUrl('settings/apps/list?category={categoryId}', {
|
||||
categoryId: categoryId
|
||||
}), {
|
||||
type:'GET',
|
||||
success: function (apps) {
|
||||
OC.Settings.Apps.State.currentCategoryElements = apps.apps;
|
||||
var appListWithIndex = _.indexBy(apps.apps, 'id');
|
||||
OC.Settings.Apps.State.apps = appListWithIndex;
|
||||
var appList = _.map(appListWithIndex, function(app) {
|
||||
// default values for missing fields
|
||||
return _.extend({level: 0}, app);
|
||||
});
|
||||
var source;
|
||||
if (categoryId === 'enabled' || categoryId === 'updates' || categoryId === 'disabled' || categoryId === 'installed' || categoryId === 'app-bundles') {
|
||||
source = $("#app-template-installed").html();
|
||||
$('#apps-list').addClass('installed');
|
||||
} else {
|
||||
source = $("#app-template").html();
|
||||
$('#apps-list').removeClass('installed');
|
||||
}
|
||||
var template = Handlebars.compile(source);
|
||||
|
||||
if (appList.length) {
|
||||
if(categoryId !== 'app-bundles') {
|
||||
appList.sort(function (a, b) {
|
||||
if (a.active !== b.active) {
|
||||
return (a.active ? -1 : 1)
|
||||
}
|
||||
if (a.update !== b.update) {
|
||||
return (a.update ? -1 : 1)
|
||||
}
|
||||
return OC.Util.naturalSortCompare(a.name, b.name);
|
||||
});
|
||||
}
|
||||
|
||||
var firstExperimental = false;
|
||||
var hasNewUpdates = false;
|
||||
_.each(appList, function(app) {
|
||||
if(app.level === 0 && firstExperimental === false) {
|
||||
firstExperimental = true;
|
||||
OC.Settings.Apps.renderApp(app, template, null, true);
|
||||
} else {
|
||||
OC.Settings.Apps.renderApp(app, template, null, false);
|
||||
}
|
||||
|
||||
if (app.update) {
|
||||
hasNewUpdates = true;
|
||||
var $update = $('#app-' + app.id + ' .update');
|
||||
$update.removeClass('hidden');
|
||||
$update.val(t('settings', 'Update to %s').replace(/%s/g, app.update));
|
||||
}
|
||||
});
|
||||
// reload updates if a list with new updates is loaded
|
||||
if (hasNewUpdates) {
|
||||
OC.Settings.Apps.reloadUpdates();
|
||||
} else {
|
||||
// hide update category after all updates are installed
|
||||
// and the user is switching away from the empty updates view
|
||||
OC.Settings.Apps.refreshUpdateCounter();
|
||||
}
|
||||
} else {
|
||||
if (categoryId === 'updates') {
|
||||
OC.Settings.Apps.showEmptyUpdates();
|
||||
} else {
|
||||
$('#apps-list').addClass('hidden');
|
||||
$('#apps-list-empty').removeClass('hidden').find('h2').text(t('settings', 'No apps found for your version'));
|
||||
$('#app-list-empty-icon').addClass('icon-search').removeClass('icon-download');
|
||||
}
|
||||
}
|
||||
|
||||
$('.enable.needs-download').tooltip({
|
||||
title: t('settings', 'The app will be downloaded from the app store'),
|
||||
placement: 'bottom',
|
||||
container: 'body'
|
||||
});
|
||||
|
||||
$('.app-level .official').tooltip({
|
||||
title: t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.'),
|
||||
placement: 'bottom',
|
||||
container: 'body'
|
||||
});
|
||||
$('.app-level .approved').tooltip({
|
||||
title: t('settings', 'Approved apps are developed by trusted developers and have passed a cursory security check. They are actively maintained in an open code repository and their maintainers deem them to be stable for casual to normal use.'),
|
||||
placement: 'bottom',
|
||||
container: 'body'
|
||||
});
|
||||
$('.app-level .experimental').tooltip({
|
||||
title: t('settings', 'This app is not checked for security issues and is new or known to be unstable. Install at your own risk.'),
|
||||
placement: 'bottom',
|
||||
container: 'body'
|
||||
});
|
||||
},
|
||||
complete: function() {
|
||||
$('#app-content').removeClass('icon-loading');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
renderApp: function(app, template, selector, firstExperimental) {
|
||||
if (!template) {
|
||||
var source = $("#app-template").html();
|
||||
template = Handlebars.compile(source);
|
||||
}
|
||||
if (typeof app === 'string') {
|
||||
app = OC.Settings.Apps.State.apps[app];
|
||||
}
|
||||
app.firstExperimental = firstExperimental;
|
||||
|
||||
if (!app.preview) {
|
||||
app.preview = OC.imagePath('core', 'places/default-app-icon');
|
||||
app.previewAsIcon = true;
|
||||
}
|
||||
|
||||
if (_.isArray(app.author)) {
|
||||
var authors = [];
|
||||
_.each(app.author, function (author) {
|
||||
if (typeof author === 'string') {
|
||||
authors.push(author);
|
||||
} else {
|
||||
authors.push(author['@value']);
|
||||
}
|
||||
});
|
||||
app.author = authors.join(', ');
|
||||
} else if (typeof app.author !== 'string') {
|
||||
app.author = app.author['@value'];
|
||||
}
|
||||
|
||||
// Parse markdown in app description
|
||||
app.description = DOMPurify.sanitize(
|
||||
marked(app.description.trim(), OC.Settings.Apps.markedOptions),
|
||||
{
|
||||
SAFE_FOR_JQUERY: true,
|
||||
ALLOWED_TAGS: [
|
||||
'strong',
|
||||
'p',
|
||||
'a',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'em',
|
||||
'del',
|
||||
'blockquote'
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
var html = template(app);
|
||||
if (selector) {
|
||||
selector.html(html);
|
||||
} else {
|
||||
$('#apps-list').append(html);
|
||||
}
|
||||
|
||||
var page = $('#app-' + app.id);
|
||||
|
||||
if (app.preview) {
|
||||
var currentImage = new Image();
|
||||
currentImage.src = app.preview;
|
||||
|
||||
currentImage.onload = function() {
|
||||
/* Trigger color inversion for placeholder image too */
|
||||
if(app.previewAsIcon) {
|
||||
page.find('.app-image')
|
||||
.append(OC.Settings.Apps.imageUrl(app.preview, false))
|
||||
.removeClass('icon-loading');
|
||||
} else {
|
||||
page.find('.app-image')
|
||||
.append(OC.Settings.Apps.imageUrl(app.preview, app.fromAppStore))
|
||||
.removeClass('icon-loading');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// set group select properly
|
||||
if(OC.Settings.Apps.isType(app, 'filesystem') || OC.Settings.Apps.isType(app, 'prelogin') ||
|
||||
OC.Settings.Apps.isType(app, 'authentication') || OC.Settings.Apps.isType(app, 'logging') ||
|
||||
OC.Settings.Apps.isType(app, 'prevent_group_restriction')) {
|
||||
page.find(".groups-enable").hide();
|
||||
page.find(".groups-enable__checkbox").prop('checked', false);
|
||||
} else {
|
||||
page.find('.group_select').val((app.groups || []).join('|'));
|
||||
if (app.active) {
|
||||
if (app.groups.length) {
|
||||
OC.Settings.Apps.setupGroupsSelect(page.find('.group_select'));
|
||||
page.find(".groups-enable__checkbox").prop('checked', true);
|
||||
} else {
|
||||
page.find(".groups-enable__checkbox").prop('checked', false);
|
||||
}
|
||||
page.find(".groups-enable").show();
|
||||
} else {
|
||||
page.find(".groups-enable").hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the image for apps listing
|
||||
* url : the url of the image
|
||||
* appfromstore: bool to check whether the app is fetched from store or not.
|
||||
*/
|
||||
|
||||
imageUrl : function (url, appfromstore) {
|
||||
var img;
|
||||
if (appfromstore) {
|
||||
img = '<svg viewBox="0 0 72 72">';
|
||||
img += '<image x="0" y="0" width="72" height="72" preserveAspectRatio="xMinYMin meet" xlink:href="' + url + '" class="app-icon" /></svg>';
|
||||
} else {
|
||||
var rnd = Math.floor((Math.random() * 100 )) + new Date().getSeconds() + new Date().getMilliseconds();
|
||||
img = '<svg width="32" height="32" viewBox="0 0 32 32">';
|
||||
img += '<defs><filter id="invertIconApps-' + rnd + '"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs>'
|
||||
img += '<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" filter="url(#invertIconApps-' + rnd + ')" xlink:href="' + url + '?v=' + oc_config.version + '" class="app-icon"></image></svg>';
|
||||
}
|
||||
return img;
|
||||
},
|
||||
|
||||
isType: function(app, type){
|
||||
return app.types && app.types.indexOf(type) !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks the server health.
|
||||
*
|
||||
* If the promise fails, the server is broken.
|
||||
*
|
||||
* @return {Promise} promise
|
||||
*/
|
||||
_checkServerHealth: function() {
|
||||
return $.get(OC.generateUrl('apps/files'));
|
||||
},
|
||||
|
||||
enableAppBundle:function(bundleId, active, element, groups) {
|
||||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
|
||||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.enableAppBundle, this, bundleId, active, element, groups), {
|
||||
text: t('settings', 'Installing apps requires you to confirm your password'),
|
||||
confirm: t('settings', 'Install app bundle'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var apps = OC.Settings.Apps.State.currentCategoryElements;
|
||||
var appsToEnable = [];
|
||||
apps.forEach(function(app) {
|
||||
if(app['bundleId'] === bundleId) {
|
||||
if(app['active'] === false) {
|
||||
appsToEnable.push(app['id']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
OC.Settings.Apps.enableApp(appsToEnable, false, groups);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string[]} appId
|
||||
* @param {boolean} active
|
||||
* @param {array} groups
|
||||
*/
|
||||
enableApp:function(appId, active, groups) {
|
||||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
|
||||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.enableApp, this, appId, active, groups), {
|
||||
text: ( active ? t('settings', 'Disabling apps requires you to confirm your password') : t('settings', 'Enabling apps requires you to confirm your password') ),
|
||||
confirm: ( active ? t('settings', 'Disable app') : t('settings', 'Enable app') ),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var elements = [];
|
||||
appId.forEach(function(appId) {
|
||||
elements.push($('#app-'+appId+' .enable'));
|
||||
});
|
||||
|
||||
var self = this;
|
||||
appId.forEach(function(appId) {
|
||||
OC.Settings.Apps.hideErrorMessage(appId);
|
||||
});
|
||||
groups = groups || [];
|
||||
var appItems = [];
|
||||
appId.forEach(function(appId) {
|
||||
appItems.push($('div#app-'+appId+''));
|
||||
});
|
||||
|
||||
if(active && !groups.length) {
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings','Disabling app …'));
|
||||
});
|
||||
$.post(OC.filePath('settings','ajax','disableapp.php'),{appid:appId},function(result) {
|
||||
if(!result || result.status !== 'success') {
|
||||
if (result.data && result.data.message) {
|
||||
OC.Settings.Apps.showErrorMessage(appId, result.data.message);
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('errormsg', result.data.message);
|
||||
})
|
||||
} else {
|
||||
OC.Settings.Apps.showErrorMessage(appId, t('settings', 'Error while disabling app'));
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('errormsg', t('settings', 'Error while disabling app'));
|
||||
});
|
||||
}
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings','Disable'));
|
||||
});
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.addClass('appwarning');
|
||||
});
|
||||
} else {
|
||||
OC.Settings.Apps.rebuildNavigation();
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('active', false);
|
||||
appItem.data('groups', '');
|
||||
});
|
||||
elements.forEach(function(element) {
|
||||
element.data('active', false);
|
||||
});
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.removeClass('active');
|
||||
});
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings', 'Enable'));
|
||||
element.parent().find(".groups-enable").hide();
|
||||
element.parent().find('.group_select').hide().val(null);
|
||||
});
|
||||
OC.Settings.Apps.State.apps[appId].active = false;
|
||||
}
|
||||
},'json');
|
||||
} else {
|
||||
// TODO: display message to admin to not refresh the page!
|
||||
// TODO: lock UI to prevent further operations
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings', 'Enabling app …'));
|
||||
});
|
||||
|
||||
var appIdArray = [];
|
||||
if( typeof appId === 'string' ) {
|
||||
appIdArray = [appId];
|
||||
} else {
|
||||
appIdArray = appId;
|
||||
}
|
||||
$.post(OC.filePath('settings','ajax','enableapp.php'),{appIds: appIdArray, groups: groups},function(result) {
|
||||
if(!result || result.status !== 'success') {
|
||||
if (result.data && result.data.message) {
|
||||
OC.Settings.Apps.showErrorMessage(appId, result.data.message);
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('errormsg', result.data.message);
|
||||
});
|
||||
} else {
|
||||
OC.Settings.Apps.showErrorMessage(appId, t('settings', 'Error while enabling app'));
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('errormsg', t('settings', 'Error while disabling app'));
|
||||
});
|
||||
}
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings', 'Enable'));
|
||||
});
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.addClass('appwarning');
|
||||
});
|
||||
} else {
|
||||
self._checkServerHealth().done(function() {
|
||||
if (result.data.update_required) {
|
||||
OC.Settings.Apps.showReloadMessage();
|
||||
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
OC.Settings.Apps.rebuildNavigation();
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('active', true);
|
||||
});
|
||||
elements.forEach(function(element) {
|
||||
element.data('active', true);
|
||||
});
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.addClass('active');
|
||||
});
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings', 'Disable'));
|
||||
});
|
||||
var app = OC.Settings.Apps.State.apps[appId];
|
||||
app.active = true;
|
||||
|
||||
if (OC.Settings.Apps.isType(app, 'filesystem') || OC.Settings.Apps.isType(app, 'prelogin') ||
|
||||
OC.Settings.Apps.isType(app, 'authentication') || OC.Settings.Apps.isType(app, 'logging')) {
|
||||
elements.forEach(function(element) {
|
||||
element.parent().find(".groups-enable").prop('checked', true);
|
||||
element.parent().find(".groups-enable").hide();
|
||||
element.parent().find('.group_select').hide().val(null);
|
||||
});
|
||||
} else {
|
||||
elements.forEach(function(element) {
|
||||
element.parent().find("#groups-enable").show();
|
||||
});
|
||||
if (groups) {
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('groups', JSON.stringify(groups));
|
||||
});
|
||||
} else {
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('groups', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
}).fail(function() {
|
||||
// server borked, emergency disable app
|
||||
$.post(OC.webroot + '/index.php/disableapp', {appid: appId}, function() {
|
||||
OC.Settings.Apps.showErrorMessage(
|
||||
appId,
|
||||
t('settings', 'Error: This app can not be enabled because it makes the server unstable')
|
||||
);
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('errormsg', t('settings', 'Error while enabling app'));
|
||||
});
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings', 'Enable'));
|
||||
});
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.addClass('appwarning');
|
||||
});
|
||||
}).fail(function() {
|
||||
OC.Settings.Apps.showErrorMessage(
|
||||
appId,
|
||||
t('settings', 'Error: Could not disable broken app')
|
||||
);
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('errormsg', t('settings', 'Error while disabling broken app'));
|
||||
});
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings', 'Enable'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},'json')
|
||||
.fail(function() {
|
||||
OC.Settings.Apps.showErrorMessage(appId, t('settings', 'Error while enabling app'));
|
||||
appItems.forEach(function(appItem) {
|
||||
appItem.data('errormsg', t('settings', 'Error while enabling app'));
|
||||
appItem.data('active', false);
|
||||
appItem.addClass('appwarning');
|
||||
});
|
||||
elements.forEach(function(element) {
|
||||
element.val(t('settings', 'Enable'));
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showEmptyUpdates: function() {
|
||||
$('#apps-list').addClass('hidden');
|
||||
$('#apps-list-empty').removeClass('hidden').find('h2').text(t('settings', 'App up to date'));
|
||||
$('#app-list-empty-icon').removeClass('icon-search').addClass('icon-download');
|
||||
},
|
||||
|
||||
updateApp:function(appId, element) {
|
||||
var oldButtonText = element.val();
|
||||
element.val(t('settings','Updating …'));
|
||||
OC.Settings.Apps.hideErrorMessage(appId);
|
||||
$.post(OC.filePath('settings','ajax','updateapp.php'),{appid:appId},function(result) {
|
||||
if(!result || result.status !== 'success') {
|
||||
if (result.data && result.data.message) {
|
||||
OC.Settings.Apps.showErrorMessage(appId, result.data.message);
|
||||
} else {
|
||||
OC.Settings.Apps.showErrorMessage(appId, t('settings','Could not update app'));
|
||||
}
|
||||
element.val(oldButtonText);
|
||||
}
|
||||
else {
|
||||
element.val(t('settings','Updated'));
|
||||
element.hide();
|
||||
|
||||
var $update = $('#app-' + appId + ' .update');
|
||||
$update.addClass('hidden');
|
||||
var $version = $('#app-' + appId + ' .app-version');
|
||||
$version.text(OC.Settings.Apps.State.apps[appId]['update']);
|
||||
|
||||
OC.Settings.Apps.State.availableUpdates--;
|
||||
OC.Settings.Apps.refreshUpdateCounter();
|
||||
|
||||
if (OC.Settings.Apps.State.currentCategory === 'updates') {
|
||||
$('#app-' + appId).remove();
|
||||
if (OC.Settings.Apps.State.availableUpdates === 0) {
|
||||
OC.Settings.Apps.showEmptyUpdates();
|
||||
}
|
||||
}
|
||||
}
|
||||
},'json');
|
||||
},
|
||||
|
||||
uninstallApp:function(appId, element) {
|
||||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
|
||||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.uninstallApp, this, appId, element), {
|
||||
text: t('settings', 'Uninstalling apps requires you to confirm your password'),
|
||||
confirm: t('settings', 'Uninstall')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
OC.Settings.Apps.hideErrorMessage(appId);
|
||||
element.val(t('settings','Removing …'));
|
||||
$.post(OC.filePath('settings','ajax','uninstallapp.php'),{appid:appId},function(result) {
|
||||
if(!result || result.status !== 'success') {
|
||||
OC.Settings.Apps.showErrorMessage(appId, t('settings','Could not remove app'));
|
||||
element.val(t('settings','Remove'));
|
||||
} else {
|
||||
OC.Settings.Apps.rebuildNavigation();
|
||||
element.parents('#apps-list > .section').fadeOut(function() {
|
||||
this.remove();
|
||||
});
|
||||
}
|
||||
},'json');
|
||||
},
|
||||
|
||||
rebuildNavigation: function() {
|
||||
$.getJSON(OC.linkToOCS('core/navigation', 2) + 'apps?format=json').done(function(response){
|
||||
if(response.ocs.meta.status === 'ok') {
|
||||
|
@ -691,334 +87,5 @@ OC.Settings.Apps = OC.Settings.Apps || {
|
|||
$(window).trigger('resize');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
reloadUpdates: function() {
|
||||
if (this._loadUpdatesCall) {
|
||||
this._loadUpdatesCall.abort();
|
||||
}
|
||||
this._loadUpdatesCall = $.ajax(OC.generateUrl('settings/apps/list?category=updates'), {
|
||||
type:'GET',
|
||||
success: function (apps) {
|
||||
OC.Settings.Apps.State.availableUpdates = apps.apps.length;
|
||||
OC.Settings.Apps.refreshUpdateCounter();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refreshUpdateCounter: function() {
|
||||
var $appCategoryUpdates = $('#app-category-updates');
|
||||
var $updateCount = $appCategoryUpdates.find('.app-navigation-entry-utils-counter');
|
||||
if (OC.Settings.Apps.State.availableUpdates > 0) {
|
||||
$updateCount.html(OC.Settings.Apps.State.availableUpdates);
|
||||
$appCategoryUpdates.show();
|
||||
} else {
|
||||
$updateCount.empty();
|
||||
if (OC.Settings.Apps.State.currentCategory !== 'updates') {
|
||||
$appCategoryUpdates.hide();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
showErrorMessage: function(appId, message) {
|
||||
$('div#app-'+appId+' .warning')
|
||||
.show()
|
||||
.text(message);
|
||||
},
|
||||
|
||||
hideErrorMessage: function(appId) {
|
||||
$('div#app-'+appId+' .warning')
|
||||
.hide()
|
||||
.text('');
|
||||
},
|
||||
|
||||
showReloadMessage: function() {
|
||||
OC.dialogs.info(
|
||||
t(
|
||||
'settings',
|
||||
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.'
|
||||
),
|
||||
t('settings','App update'),
|
||||
function () {
|
||||
window.location.reload();
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Splits the query by spaces and tries to find all substring in the app
|
||||
* @param {string} string
|
||||
* @param {string} query
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_search: function(string, query) {
|
||||
var keywords = query.split(' '),
|
||||
stringLower = string.toLowerCase(),
|
||||
found = true;
|
||||
|
||||
_.each(keywords, function(keyword) {
|
||||
found = found && stringLower.indexOf(keyword) !== -1;
|
||||
});
|
||||
|
||||
return found;
|
||||
},
|
||||
|
||||
filter: function(query) {
|
||||
var $appList = $('#apps-list'),
|
||||
$emptyList = $('#apps-list-empty');
|
||||
$('#app-list-empty-icon').addClass('icon-search').removeClass('icon-download');
|
||||
$appList.removeClass('hidden');
|
||||
$appList.find('.section').removeClass('hidden');
|
||||
$emptyList.addClass('hidden');
|
||||
|
||||
if (query === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
query = query.toLowerCase();
|
||||
$appList.find('.section').addClass('hidden');
|
||||
|
||||
// App Name
|
||||
var apps = _.filter(OC.Settings.Apps.State.apps, function (app) {
|
||||
return OC.Settings.Apps._search(app.name, query);
|
||||
});
|
||||
|
||||
// App ID
|
||||
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
|
||||
return OC.Settings.Apps._search(app.id, query);
|
||||
}));
|
||||
|
||||
// App Description
|
||||
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
|
||||
return OC.Settings.Apps._search(app.description, query);
|
||||
}));
|
||||
|
||||
// Author Name
|
||||
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
|
||||
var authors = [];
|
||||
if (_.isArray(app.author)) {
|
||||
_.each(app.author, function (author) {
|
||||
if (typeof author === 'string') {
|
||||
authors.push(author);
|
||||
} else {
|
||||
authors.push(author['@value']);
|
||||
if (!_.isUndefined(author['@attributes']['homepage'])) {
|
||||
authors.push(author['@attributes']['homepage']);
|
||||
}
|
||||
if (!_.isUndefined(author['@attributes']['mail'])) {
|
||||
authors.push(author['@attributes']['mail']);
|
||||
}
|
||||
}
|
||||
});
|
||||
return OC.Settings.Apps._search(authors.join(' '), query);
|
||||
} else if (typeof app.author !== 'string') {
|
||||
authors.push(app.author['@value']);
|
||||
if (!_.isUndefined(app.author['@attributes']['homepage'])) {
|
||||
authors.push(app.author['@attributes']['homepage']);
|
||||
}
|
||||
if (!_.isUndefined(app.author['@attributes']['mail'])) {
|
||||
authors.push(app.author['@attributes']['mail']);
|
||||
}
|
||||
return OC.Settings.Apps._search(authors.join(' '), query);
|
||||
}
|
||||
return OC.Settings.Apps._search(app.author, query);
|
||||
}));
|
||||
|
||||
// App status
|
||||
if (t('settings', 'Official').toLowerCase().indexOf(query) !== -1) {
|
||||
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
|
||||
return app.level === 200;
|
||||
}));
|
||||
}
|
||||
if (t('settings', 'Approved').toLowerCase().indexOf(query) !== -1) {
|
||||
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
|
||||
return app.level === 100;
|
||||
}));
|
||||
}
|
||||
if (t('settings', 'Experimental').toLowerCase().indexOf(query) !== -1) {
|
||||
apps = apps.concat(_.filter(OC.Settings.Apps.State.apps, function (app) {
|
||||
return app.level !== 100 && app.level !== 200;
|
||||
}));
|
||||
}
|
||||
|
||||
apps = _.uniq(apps, function(app){return app.id;});
|
||||
|
||||
if (apps.length === 0) {
|
||||
$appList.addClass('hidden');
|
||||
$emptyList.removeClass('hidden');
|
||||
$emptyList.removeClass('hidden').find('h2').text(t('settings', 'No apps found for {query}', {
|
||||
query: query
|
||||
}));
|
||||
} else {
|
||||
_.each(apps, function (app) {
|
||||
$('#app-' + app.id).removeClass('hidden');
|
||||
});
|
||||
|
||||
$('#searchresults').hide();
|
||||
}
|
||||
},
|
||||
|
||||
_onPopState: function(params) {
|
||||
params = _.extend({
|
||||
category: 'enabled'
|
||||
}, params);
|
||||
|
||||
OC.Settings.Apps.loadCategory(params.category);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the apps list
|
||||
*/
|
||||
initialize: function($el) {
|
||||
|
||||
var renderer = new marked.Renderer();
|
||||
renderer.link = function(href, title, text) {
|
||||
try {
|
||||
var prot = decodeURIComponent(unescape(href))
|
||||
.replace(/[^\w:]/g, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (prot.indexOf('http:') !== 0 && prot.indexOf('https:') !== 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var out = '<a href="' + href + '" rel="noreferrer noopener"';
|
||||
if (title) {
|
||||
out += ' title="' + title + '"';
|
||||
}
|
||||
out += '>' + text + '</a>';
|
||||
return out;
|
||||
};
|
||||
renderer.image = function(href, title, text) {
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
return title;
|
||||
};
|
||||
renderer.blockquote = function(quote) {
|
||||
return quote;
|
||||
};
|
||||
|
||||
OC.Settings.Apps.markedOptions = {
|
||||
renderer: renderer,
|
||||
gfm: false,
|
||||
highlight: false,
|
||||
tables: false,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false
|
||||
};
|
||||
|
||||
OC.Plugins.register('OCA.Search', OC.Settings.Apps.Search);
|
||||
OC.Settings.Apps.loadCategories();
|
||||
OC.Util.History.addOnPopStateHandler(_.bind(this._onPopState, this));
|
||||
|
||||
$(document).on('click', 'ul#apps-categories li', function () {
|
||||
var categoryId = $(this).data('categoryId');
|
||||
OC.Settings.Apps.loadCategory(categoryId);
|
||||
OC.Util.History.pushState({
|
||||
category: categoryId
|
||||
});
|
||||
$('#searchbox').val('');
|
||||
});
|
||||
|
||||
$(document).on('click', '.app-description-toggle-show', function () {
|
||||
$(this).addClass('hidden');
|
||||
$(this).siblings('.app-description-toggle-hide').removeClass('hidden');
|
||||
$(this).siblings('.app-description-container').slideDown();
|
||||
});
|
||||
$(document).on('click', '.app-description-toggle-hide', function () {
|
||||
$(this).addClass('hidden');
|
||||
$(this).siblings('.app-description-toggle-show').removeClass('hidden');
|
||||
$(this).siblings('.app-description-container').slideUp();
|
||||
});
|
||||
|
||||
$(document).on('click', '#apps-list input.enable', function () {
|
||||
var appId = $(this).data('appid');
|
||||
var bundleId = $(this).data('bundleid');
|
||||
var element = $(this);
|
||||
var active = $(this).data('active');
|
||||
|
||||
var category = $('#app-navigation').attr('data-category');
|
||||
if(bundleId) {
|
||||
OC.Settings.Apps.enableAppBundle(bundleId, active, element);
|
||||
element.val(t('settings', 'Enable all'));
|
||||
} else {
|
||||
OC.Settings.Apps.enableApp([appId], active);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '#apps-list input.uninstall', function () {
|
||||
var appId = $(this).data('appid');
|
||||
var element = $(this);
|
||||
|
||||
OC.Settings.Apps.uninstallApp(appId, element);
|
||||
});
|
||||
|
||||
$(document).on('click', '#apps-list input.update', function () {
|
||||
var appId = $(this).data('appid');
|
||||
var element = $(this);
|
||||
|
||||
OC.Settings.Apps.updateApp(appId, element);
|
||||
});
|
||||
|
||||
$(document).on('change', '.group_select', function() {
|
||||
var element = $(this).closest('.section').find('input.enable');
|
||||
var groups = $(this).val();
|
||||
if (groups && groups !== '') {
|
||||
groups = groups.split('|');
|
||||
} else {
|
||||
groups = [];
|
||||
}
|
||||
|
||||
var appId = element.data('appid');
|
||||
if (appId) {
|
||||
OC.Settings.Apps.enableApp([appId], false, groups);
|
||||
OC.Settings.Apps.State.apps[appId].groups = groups;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('change', ".groups-enable__checkbox", function() {
|
||||
var $select = $(this).closest('.section').find('.group_select');
|
||||
$select.val('');
|
||||
|
||||
if (this.checked) {
|
||||
OC.Settings.Apps.setupGroupsSelect($select);
|
||||
} else {
|
||||
$select.select2('destroy');
|
||||
}
|
||||
|
||||
$select.change();
|
||||
});
|
||||
|
||||
$(document).on('click', '#enable-experimental-apps', function () {
|
||||
var state = $(this).prop('checked');
|
||||
$.ajax(OC.generateUrl('settings/apps/experimental'), {
|
||||
data: {state: state},
|
||||
type: 'POST',
|
||||
success:function () {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
OC.Settings.Apps.Search = {
|
||||
attach: function (search) {
|
||||
search.setFilter('settings', OC.Settings.Apps.filter);
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready(function () {
|
||||
// HACK: FIXME: use plugin approach
|
||||
if (!window.TESTING) {
|
||||
OC.Settings.Apps.initialize($('#apps-list'));
|
||||
}
|
||||
});
|
||||
|
|
3493
settings/js/main.js
3493
settings/js/main.js
File diff suppressed because one or more lines are too long
|
@ -46,9 +46,22 @@ $application->registerRoutes($this, [
|
|||
['name' => 'MailSettings#storeCredentials', 'url' => '/settings/admin/mailsettings/credentials', 'verb' => 'POST'],
|
||||
['name' => 'MailSettings#sendTestMail', 'url' => '/settings/admin/mailtest', 'verb' => 'POST'],
|
||||
['name' => 'Encryption#startMigration', 'url' => '/settings/admin/startmigration', 'verb' => 'POST'],
|
||||
|
||||
['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'],
|
||||
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET'],
|
||||
['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'],
|
||||
['name' => 'AppSettings#enableApp', 'url' => '/settings/apps/enable/{appId}', 'verb' => 'GET'],
|
||||
['name' => 'AppSettings#enableApp', 'url' => '/settings/apps/enable/{appId}', 'verb' => 'POST'],
|
||||
['name' => 'AppSettings#enableApps', 'url' => '/settings/apps/enable', 'verb' => 'POST'],
|
||||
['name' => 'AppSettings#disableApp', 'url' => '/settings/apps/disable/{appId}', 'verb' => 'GET'],
|
||||
['name' => 'AppSettings#disableApps', 'url' => '/settings/apps/disable', 'verb' => 'POST'],
|
||||
['name' => 'AppSettings#updateApp', 'url' => '/settings/apps/update/{appId}', 'verb' => 'GET'],
|
||||
['name' => 'AppSettings#uninstallApp', 'url' => '/settings/apps/uninstall/{appId}', 'verb' => 'GET'],
|
||||
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps/{category}', 'verb' => 'GET', 'defaults' => ['category' => '']],
|
||||
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps/{category}/{id}', 'verb' => 'GET', 'defaults' => ['category' => '', 'id' => '']],
|
||||
|
||||
['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'],
|
||||
['name' => 'Users#setEMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'],
|
||||
['name' => 'Users#setUserSettings', 'url' => '/settings/users/{username}/settings', 'verb' => 'PUT'],
|
||||
['name' => 'Users#getVerificationCode', 'url' => '/settings/users/{account}/verify', 'verb' => 'GET'],
|
||||
['name' => 'Users#usersList', 'url' => '/settings/users', 'verb' => 'GET'],
|
||||
|
@ -76,13 +89,4 @@ $application->registerRoutes($this, [
|
|||
// Settings pages
|
||||
$this->create('settings_help', '/settings/help')
|
||||
->actionInclude('settings/help.php');
|
||||
// Settings ajax actions
|
||||
// apps
|
||||
$this->create('settings_ajax_enableapp', '/settings/ajax/enableapp.php')
|
||||
->actionInclude('settings/ajax/enableapp.php');
|
||||
$this->create('settings_ajax_disableapp', '/settings/ajax/disableapp.php')
|
||||
->actionInclude('settings/ajax/disableapp.php');
|
||||
$this->create('settings_ajax_updateapp', '/settings/ajax/updateapp.php')
|
||||
->actionInclude('settings/ajax/updateapp.php');
|
||||
$this->create('settings_ajax_uninstallapp', '/settings/ajax/uninstallapp.php')
|
||||
->actionInclude('settings/ajax/uninstallapp.php');
|
||||
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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 id="app-details-view" style="padding: 20px;">
|
||||
<a class="close icon-close" href="#" v-on:click="hideAppDetails"><span class="hidden-visually">Close</span></a>
|
||||
<h2>
|
||||
<div v-if="!app.preview" class="icon-settings-dark"></div>
|
||||
<svg v-if="app.previewAsIcon && app.preview" width="32" height="32" viewBox="0 0 32 32">
|
||||
<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs>
|
||||
<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" :filter="filterUrl" :xlink:href="app.preview" class="app-icon"></image>
|
||||
</svg>
|
||||
{{ app.name }}</h2>
|
||||
<img v-if="app.screenshot" :src="app.screenshot" width="100%" />
|
||||
<div class="app-level" v-if="app.level === 200 || hasRating">
|
||||
<span class="official icon-checkmark" v-if="app.level === 200"
|
||||
v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')">
|
||||
{{ t('settings', 'Official') }}</span>
|
||||
<app-score v-if="hasRating" :score="app.appstoreData.ratingOverall"></app-score>
|
||||
</div>
|
||||
|
||||
<div class="app-author" v-if="author">
|
||||
{{ t('settings', 'by') }}
|
||||
<span v-for="(a, index) in author">
|
||||
<a v-if="a['@attributes'] && a['@attributes']['homepage']" :href="a['@attributes']['homepage']">{{ a['@value'] }}</a><span v-else-if="a['@value']">{{ a['@value'] }}</span><span v-else>{{ a }}</span><span v-if="index+1 < author.length">, </span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="app-licence" v-if="licence">{{ licence }}</div>
|
||||
<div class="actions">
|
||||
<div class="actions-buttons">
|
||||
<input v-if="app.update" class="update" type="button" :value="t('settings', 'Update to {version}', {version: app.update})" :disabled="installing || loading(app.id)"/>
|
||||
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" :disabled="installing || loading(app.id)"/>
|
||||
<input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click="disable(app.id)" :disabled="installing || loading(app.id)" />
|
||||
<input v-if="!app.active" class="enable" type="button" :value="enableButtonText" v-on:click="enable(app.id)" v-tooltip.auto="enableButtonTooltip" :disabled="!app.canInstall || installing || loading(app.id)" />
|
||||
</div>
|
||||
<div class="app-groups">
|
||||
<div class="groups-enable" v-if="app.active && canLimitToGroups(app)">
|
||||
<input type="checkbox" :value="app.id" v-model="groupCheckedAppsData" v-on:change="setGroupLimit" class="groups-enable__checkbox checkbox" :id="prefix('groups_enable', app.id)">
|
||||
<label :for="prefix('groups_enable', app.id)">Auf Gruppen beschränken</label>
|
||||
<input type="hidden" class="group_select" title="Alle" value="">
|
||||
<multiselect v-if="isLimitedToGroups(app)" :options="groups" :value="appGroups" @select="addGroupLimitation" @remove="removeGroupLimitation" :options-limit="5"
|
||||
:placeholder="t('settings', 'Limit app usage to groups')"
|
||||
label="name" track-by="id" class="multiselect-vue"
|
||||
:multiple="true" :close-on-select="false">
|
||||
<span slot="noResult">{{t('settings', 'No results')}}</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="documentation">
|
||||
<a class="appslink" :href="appstoreUrl" v-if="!app.internal" target="_blank" rel="noreferrer noopener">{{ t('settings', 'View in store')}} ↗</a>
|
||||
|
||||
<a class="appslink" v-if="app.website" :href="app.website" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Visit website') }} ↗</a>
|
||||
<a class="appslink" v-if="app.bugs" :href="app.bugs" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Report a bug') }} ↗</a>
|
||||
|
||||
<a class="appslink" v-if="app.documentation && app.documentation.user" :href="app.documentation.user" target="_blank" rel="noreferrer noopener">{{ t('settings', 'User documentation') }} ↗</a>
|
||||
<a class="appslink" v-if="app.documentation && app.documentation.admin" :href="app.documentation.admin" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Admin documentation') }} ↗</a>
|
||||
<a class="appslink" v-if="app.documentation && app.documentation.developer" :href="app.documentation.developer" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} ↗</a>
|
||||
</p>
|
||||
|
||||
<ul class="app-dependencies">
|
||||
<li v-if="app.missingMinOwnCloudVersion">{{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}</li>
|
||||
<li v-if="app.missingMaxOwnCloudVersion">{{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }}</li>
|
||||
<li v-if="!app.canInstall">
|
||||
{{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
|
||||
<ul class="missing-dependencies">
|
||||
<li v-for="dep in app.missingDependencies">{{ dep }}</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="app-description" v-html="renderMarkdown"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import AppScore from './appList/appScore';
|
||||
import AppManagement from './appManagement';
|
||||
import prefix from './prefixMixin';
|
||||
import SvgFilterMixin from './svgFilterMixin';
|
||||
|
||||
export default {
|
||||
mixins: [AppManagement, prefix, SvgFilterMixin],
|
||||
name: 'appDetails',
|
||||
props: ['category', 'app'],
|
||||
components: {
|
||||
Multiselect,
|
||||
AppScore
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groupCheckedAppsData: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.app.groups.length > 0) {
|
||||
this.groupCheckedAppsData = true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hideAppDetails() {
|
||||
this.$router.push({
|
||||
name: 'apps-category',
|
||||
params: {category: this.category}
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
appstoreUrl() {
|
||||
return `https://apps.nextcloud.com/apps/${this.app.id}`;
|
||||
},
|
||||
licence() {
|
||||
if (this.app.licence)
|
||||
return ('' + this.app.licence).toUpperCase() + t('settings', '-licensed');
|
||||
return null;
|
||||
},
|
||||
hasRating() {
|
||||
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5;
|
||||
},
|
||||
author() {
|
||||
if (typeof this.app.author === 'string') {
|
||||
return [
|
||||
{
|
||||
'@value': this.app.author
|
||||
}
|
||||
]
|
||||
}
|
||||
if (this.app.author['@value']) {
|
||||
return [this.app.author];
|
||||
}
|
||||
return this.app.author;
|
||||
},
|
||||
appGroups() {
|
||||
return this.app.groups.map(group => {return {id: group, name: group}});
|
||||
},
|
||||
groups() {
|
||||
return this.$store.getters.getGroups
|
||||
.filter(group => group.id !== 'disabled')
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
renderMarkdown() {
|
||||
// TODO: bundle marked as well
|
||||
var renderer = new window.marked.Renderer();
|
||||
renderer.link = function(href, title, text) {
|
||||
try {
|
||||
var prot = decodeURIComponent(unescape(href))
|
||||
.replace(/[^\w:]/g, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (prot.indexOf('http:') !== 0 && prot.indexOf('https:') !== 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var out = '<a href="' + href + '" rel="noreferrer noopener"';
|
||||
if (title) {
|
||||
out += ' title="' + title + '"';
|
||||
}
|
||||
out += '>' + text + '</a>';
|
||||
return out;
|
||||
};
|
||||
renderer.image = function(href, title, text) {
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
return title;
|
||||
};
|
||||
renderer.blockquote = function(quote) {
|
||||
return quote;
|
||||
};
|
||||
return DOMPurify.sanitize(
|
||||
window.marked(this.app.description.trim(), {
|
||||
renderer: renderer,
|
||||
gfm: false,
|
||||
highlight: false,
|
||||
tables: false,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false
|
||||
}),
|
||||
{
|
||||
SAFE_FOR_JQUERY: true,
|
||||
ALLOWED_TAGS: [
|
||||
'strong',
|
||||
'p',
|
||||
'a',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'em',
|
||||
'del',
|
||||
'blockquote'
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,181 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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 id="app-content-inner">
|
||||
<div id="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
|
||||
<template v-if="useListView">
|
||||
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" />
|
||||
</template>
|
||||
<template v-for="bundle in bundles" v-if="useBundleView && bundleApps(bundle.id).length > 0">
|
||||
<div class="apps-header">
|
||||
<div class="app-image"></div>
|
||||
<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" v-on:click="toggleBundle(bundle.id)"></h2>
|
||||
<div class="app-version"></div>
|
||||
<div class="app-level"></div>
|
||||
<div class="app-groups"></div>
|
||||
<div class="actions"> </div>
|
||||
</div>
|
||||
<app-item v-for="app in bundleApps(bundle.id)" :key="bundle.id + app.id" :app="app" :category="category"/>
|
||||
</template>
|
||||
<template v-if="useAppStoreView">
|
||||
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" :list-view="false" />
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="apps-list-search" class="installed">
|
||||
<template v-if="search !== '' && searchApps.length > 0">
|
||||
<div class="section">
|
||||
<div></div>
|
||||
<h2>{{ t('settings', 'Results from other categories') }}</h2>
|
||||
</div>
|
||||
<app-item v-for="app in searchApps" :key="app.id" :app="app" :category="category" :list-view="true" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div id="apps-list-empty" class="emptycontent emptycontent-search" v-if="!loading && searchApps.length === 0 && apps.length === 0">
|
||||
<div id="app-list-empty-icon" class="icon-settings-dark"></div>
|
||||
<h2>{{ t('settings', 'No apps found for your versoin')}}</h2>
|
||||
</div>
|
||||
|
||||
<div id="searchresults"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appItem from './appList/appItem';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import prefix from './prefixMixin';
|
||||
|
||||
export default {
|
||||
name: 'appList',
|
||||
mixins: [prefix],
|
||||
props: ['category', 'app', 'search'],
|
||||
components: {
|
||||
Multiselect,
|
||||
appItem
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return this.$store.getters.loading('list');
|
||||
},
|
||||
apps() {
|
||||
let apps = this.$store.getters.getAllApps
|
||||
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
|
||||
.sort(function (a, b) {
|
||||
if (a.active !== b.active) {
|
||||
return (a.active ? -1 : 1)
|
||||
}
|
||||
if (a.update !== b.update) {
|
||||
return (a.update ? -1 : 1)
|
||||
}
|
||||
return OC.Util.naturalSortCompare(a.name, b.name);
|
||||
});
|
||||
|
||||
if (this.category === 'installed') {
|
||||
return apps.filter(app => app.installed);
|
||||
}
|
||||
if (this.category === 'enabled') {
|
||||
return apps.filter(app => app.active);
|
||||
}
|
||||
if (this.category === 'disabled') {
|
||||
return apps.filter(app => !app.active);
|
||||
}
|
||||
if (this.category === 'app-bundles') {
|
||||
return apps.filter(app => app.bundles);
|
||||
}
|
||||
if (this.category === 'updates') {
|
||||
return apps.filter(app => app.update);
|
||||
}
|
||||
// filter app store categories
|
||||
return apps.filter(app => {
|
||||
return app.appstore && app.category !== undefined &&
|
||||
(app.category === this.category || app.category.indexOf(this.category) > -1);
|
||||
});
|
||||
},
|
||||
bundles() {
|
||||
return this.$store.getters.getServerData.bundles;
|
||||
},
|
||||
bundleApps() {
|
||||
return function(bundle) {
|
||||
return this.$store.getters.getAllApps
|
||||
.filter(app => app.bundleId === bundle);
|
||||
}
|
||||
},
|
||||
searchApps() {
|
||||
if (this.search === '') {
|
||||
return [];
|
||||
}
|
||||
return this.$store.getters.getAllApps
|
||||
.filter(app => {
|
||||
if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
|
||||
return (!this.apps.find(_app => _app.id === app.id));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
useAppStoreView() {
|
||||
return !this.useListView && !this.useBundleView;
|
||||
},
|
||||
useListView() {
|
||||
return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates');
|
||||
},
|
||||
useBundleView() {
|
||||
return (this.category === 'app-bundles');
|
||||
},
|
||||
allBundlesEnabled() {
|
||||
let self = this;
|
||||
return function(id) {
|
||||
return self.bundleApps(id).filter(app => !app.active).length === 0;
|
||||
}
|
||||
},
|
||||
bundleToggleText() {
|
||||
let self = this;
|
||||
return function(id) {
|
||||
if (self.allBundlesEnabled(id)) {
|
||||
return t('settings', 'Disable all');
|
||||
}
|
||||
return t('settings', 'Enable all');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleBundle(id) {
|
||||
if (this.allBundlesEnabled(id)) {
|
||||
return this.disableBundle(id);
|
||||
}
|
||||
return this.enableBundle(id);
|
||||
},
|
||||
enableBundle(id) {
|
||||
let apps = this.bundleApps(id).map(app => app.id);
|
||||
this.$store.dispatch('enableApp', { appId: apps, groups: [] })
|
||||
.catch((error) => { console.log(error); OC.Notification.show(error)});
|
||||
},
|
||||
disableBundle(id) {
|
||||
let apps = this.bundleApps(id).map(app => app.id);
|
||||
this.$store.dispatch('disableApp', { appId: apps, groups: [] })
|
||||
.catch((error) => { OC.Notification.show(error)});
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,118 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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="section" v-bind:class="{ selected: isSelected }" v-on:click="showAppDetails">
|
||||
<div class="app-image app-image-icon" v-on:click="showAppDetails">
|
||||
<div v-if="(listView && !app.preview) || (!listView && !app.screenshot)" class="icon-settings-dark"></div>
|
||||
|
||||
<svg v-if="listView && app.preview" width="32" height="32" viewBox="0 0 32 32">
|
||||
<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs>
|
||||
<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" :filter="filterUrl" :xlink:href="app.preview" class="app-icon"></image>
|
||||
</svg>
|
||||
|
||||
<img v-if="!listView && app.screenshot" :src="app.screenshot" width="100%" />
|
||||
</div>
|
||||
<div class="app-name" v-on:click="showAppDetails">
|
||||
{{ app.name }}
|
||||
</div>
|
||||
<div class="app-summary" v-if="!listView">{{ app.summary }}</div>
|
||||
<div class="app-version" v-if="listView">
|
||||
<span v-if="app.version">{{ app.version }}</span>
|
||||
<span v-else-if="app.appstoreData.releases[0].version">{{ app.appstoreData.releases[0].version }}</span>
|
||||
</div>
|
||||
|
||||
<div class="app-level">
|
||||
<span class="official icon-checkmark" v-if="app.level === 200"
|
||||
v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')">
|
||||
{{ t('settings', 'Official') }}</span>
|
||||
<app-score v-if="!listView" :score="app.score"></app-score>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="warning" v-if="app.error">{{ app.error }}</div>
|
||||
<div class="icon icon-loading-small" v-if="loading(app.id)"></div>
|
||||
<input v-if="app.update" class="update" type="button" :value="t('settings', 'Update to {update}', {update:app.update})" v-on:click="update(app.id)" :disabled="installing || loading(app.id)" />
|
||||
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" v-on:click="remove(app.id)" :disabled="installing || loading(app.id)" />
|
||||
<input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click="disable(app.id)" :disabled="installing || loading(app.id)" />
|
||||
<input v-if="!app.active" class="enable" type="button" :value="enableButtonText" v-on:click="enable(app.id)" v-tooltip.auto="enableButtonTooltip" :disabled="!app.canInstall || installing || loading(app.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import AppScore from './appScore';
|
||||
import AppManagement from '../appManagement';
|
||||
import SvgFilterMixin from '../svgFilterMixin';
|
||||
|
||||
export default {
|
||||
name: 'appItem',
|
||||
mixins: [AppManagement, SvgFilterMixin],
|
||||
props: {
|
||||
app: {},
|
||||
category: {},
|
||||
listView: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id': function (id) {
|
||||
this.isSelected = (this.app.id === id);
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Multiselect,
|
||||
AppScore,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSelected: false,
|
||||
scrolled: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.isSelected = (this.app.id === this.$route.params.id);
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
watchers: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
showAppDetails(event) {
|
||||
if (event.currentTarget.tagName === 'INPUT' || event.currentTarget.tagName === 'A') {
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
name: 'apps-details',
|
||||
params: {category: this.category, id: this.app.id}
|
||||
});
|
||||
},
|
||||
prefix(prefix, content) {
|
||||
return prefix + '_' + content;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,38 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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>
|
||||
<img :src="scoreImage" class="app-score-image" />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'appScore',
|
||||
props: ['score'],
|
||||
computed: {
|
||||
scoreImage() {
|
||||
let score = Math.round( this.score * 10 );
|
||||
let imageName = 'rating/s' + score + '.svg';
|
||||
return OC.imagePath('core', imageName);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,117 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
if (this.app.groups.length > 0) {
|
||||
this.groupCheckedAppsData = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
appGroups() {
|
||||
return this.app.groups.map(group => {return {id: group, name: group}});
|
||||
},
|
||||
loading() {
|
||||
let self = this;
|
||||
return function(id) {
|
||||
return self.$store.getters.loading(id);
|
||||
}
|
||||
},
|
||||
installing() {
|
||||
return this.$store.getters.loading('install');
|
||||
},
|
||||
enableButtonText() {
|
||||
if (this.app.needsDownload) {
|
||||
return t('settings','Download and enable');
|
||||
}
|
||||
return t('settings','Enable');
|
||||
},
|
||||
enableButtonTooltip() {
|
||||
if (this.app.needsDownload) {
|
||||
return t('settings','The app will be downloaded from the app store');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isLimitedToGroups(app) {
|
||||
if (this.app.groups.length || this.groupCheckedAppsData) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
setGroupLimit: function() {
|
||||
if (!this.groupCheckedAppsData) {
|
||||
this.$store.dispatch('enableApp', {appId: this.app.id, groups: []});
|
||||
}
|
||||
},
|
||||
canLimitToGroups(app) {
|
||||
if (app.types && app.types.includes('filesystem')
|
||||
|| app.types.includes('prelogin')
|
||||
|| app.types.includes('authentication')
|
||||
|| app.types.includes('logging')
|
||||
|| app.types.includes('prevent_group_restriction')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
addGroupLimitation(group) {
|
||||
let groups = this.app.groups.concat([]).concat([group.id]);
|
||||
this.$store.dispatch('enableApp', { appId: this.app.id, groups: groups});
|
||||
},
|
||||
removeGroupLimitation(group) {
|
||||
let currentGroups = this.app.groups.concat([]);
|
||||
let index = currentGroups.indexOf(group.id);
|
||||
if (index > -1) {
|
||||
currentGroups.splice(index, 1);
|
||||
}
|
||||
this.$store.dispatch('enableApp', { appId: this.app.id, groups: currentGroups});
|
||||
},
|
||||
enable(appId) {
|
||||
this.$store.dispatch('enableApp', { appId: appId, groups: [] })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
|
||||
.catch((error) => { OC.Notification.show(error)});
|
||||
},
|
||||
disable(appId) {
|
||||
this.$store.dispatch('disableApp', { appId: appId })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
|
||||
.catch((error) => { OC.Notification.show(error)});
|
||||
},
|
||||
remove(appId) {
|
||||
this.$store.dispatch('uninstallApp', { appId: appId })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
|
||||
.catch((error) => { OC.Notification.show(error)});
|
||||
},
|
||||
install(appId) {
|
||||
this.$store.dispatch('enableApp', { appId: appId })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
|
||||
.catch((error) => { OC.Notification.show(error)});
|
||||
},
|
||||
update(appId) {
|
||||
this.$store.dispatch('updateApp', { appId: appId })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
|
||||
.catch((error) => { OC.Notification.show(error)});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -6,7 +6,7 @@
|
|||
<ul :id="menu.id">
|
||||
<navigation-item v-for="item in menu.items" :item="item" :key="item.key" />
|
||||
</ul>
|
||||
<div id="app-settings">
|
||||
<div id="app-settings" v-if="!!$slots['settings-content']">
|
||||
<div id="app-settings-header">
|
||||
<button class="settings-button"
|
||||
data-apps-slide-toggle="#app-settings-content"
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'prefixMixin',
|
||||
methods: {
|
||||
prefix (prefix, content) {
|
||||
return prefix + '_' + content;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import Users from './views/Users';
|
||||
import Apps from './views/Apps';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
|
@ -32,6 +33,26 @@ export default new Router({
|
|||
component: Users
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:index(index.php/)?settings/apps',
|
||||
component: Apps,
|
||||
props: true,
|
||||
name: 'apps',
|
||||
children: [
|
||||
{
|
||||
path: ':category',
|
||||
name: 'apps-category',
|
||||
component: Apps,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
name: 'apps-details',
|
||||
component: Apps
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @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 api from './api';
|
||||
import axios from 'axios/index';
|
||||
import Vue from 'vue';
|
||||
|
||||
const state = {
|
||||
apps: [],
|
||||
categories: [],
|
||||
updateCount: 0,
|
||||
loading: {},
|
||||
loadingList: false,
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
|
||||
APPS_API_FAILURE(state, error) {
|
||||
OC.Notification.showHtml(t('settings','An error occured during the request. Unable to proceed.')+'<br>'+error.error.response.data.data.message, {timeout: 7});
|
||||
console.log(state, error);
|
||||
},
|
||||
|
||||
initCategories(state, {categories, updateCount}) {
|
||||
state.categories = categories;
|
||||
state.updateCount = updateCount;
|
||||
},
|
||||
|
||||
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}) {
|
||||
let app = state.apps.find(app => app.id === appId);
|
||||
app.error = error;
|
||||
},
|
||||
|
||||
clearError(state, {appId, error}) {
|
||||
let app = state.apps.find(app => app.id === appId);
|
||||
app.error = null;
|
||||
},
|
||||
|
||||
enableApp(state, {appId, groups}) {
|
||||
let app = state.apps.find(app => app.id === appId);
|
||||
app.active = true;
|
||||
app.groups = groups;
|
||||
},
|
||||
|
||||
disableApp(state, appId) {
|
||||
let 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).canUnInstall = false;
|
||||
state.apps.find(app => app.id === appId).canInstall = true;
|
||||
},
|
||||
|
||||
updateApp(state, appId) {
|
||||
let app = state.apps.find(app => app.id === appId);
|
||||
let 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;
|
||||
},
|
||||
getUpdateCount(state) {
|
||||
return state.updateCount;
|
||||
}
|
||||
};
|
||||
|
||||
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(OC.generateUrl(`settings/apps/enable`), {appIds: apps, groups: groups})
|
||||
.then((response) => {
|
||||
context.commit('stopLoading', apps);
|
||||
context.commit('stopLoading', 'install');
|
||||
apps.forEach(_appId => {
|
||||
context.commit('enableApp', {appId: _appId, groups: groups});
|
||||
});
|
||||
|
||||
// check for server health
|
||||
return api.get(OC.generateUrl('apps/files'))
|
||||
.then(() => {
|
||||
if (response.data.update_required) {
|
||||
OC.dialogs.info(
|
||||
t(
|
||||
'settings',
|
||||
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.'
|
||||
),
|
||||
t('settings','App update'),
|
||||
function () {
|
||||
window.location.reload();
|
||||
},
|
||||
true
|
||||
);
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!Array.isArray(appId)) {
|
||||
context.commit('setError', {
|
||||
appId: apps,
|
||||
error: t('settings', 'Error: This app can not be enabled because it makes the server unstable')
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('setError', {appId: apps, error: t('settings', 'Error while enabling app')});
|
||||
context.commit('stopLoading', apps);
|
||||
context.commit('stopLoading', 'install');
|
||||
context.commit('APPS_API_FAILURE', { appId, error })
|
||||
})
|
||||
}).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(OC.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(OC.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(OC.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(OC.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))
|
||||
},
|
||||
|
||||
getCategories(context) {
|
||||
context.commit('startLoading', 'categories');
|
||||
return api.get(OC.generateUrl('settings/apps/categories'))
|
||||
.then((response) => {
|
||||
if (response.data.length > 0) {
|
||||
context.commit('appendCategories', response.data);
|
||||
context.commit('stopLoading', 'categories');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.catch((error) => context.commit('API_FAILURE', error));
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default { state, mutations, getters, actions };
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import users from './users';
|
||||
import apps from './apps';
|
||||
import settings from './settings';
|
||||
import oc from './oc';
|
||||
|
||||
|
@ -23,6 +24,7 @@ const mutations = {
|
|||
export default new Vuex.Store({
|
||||
modules: {
|
||||
users,
|
||||
apps,
|
||||
settings,
|
||||
oc
|
||||
},
|
||||
|
|
|
@ -48,12 +48,12 @@ const mutations = {
|
|||
state.groups = orderGroups(state.groups, state.orderBy);
|
||||
|
||||
},
|
||||
addGroup(state, gid) {
|
||||
addGroup(state, {gid, displayName}) {
|
||||
try {
|
||||
// extend group to default values
|
||||
let group = Object.assign({}, defaults.group, {
|
||||
id: gid,
|
||||
name: gid
|
||||
name: displayName,
|
||||
});
|
||||
state.groups.push(group);
|
||||
state.groups = orderGroups(state.groups, state.orderBy);
|
||||
|
@ -197,6 +197,21 @@ const actions = {
|
|||
.catch((error) => context.commit('API_FAILURE', error));
|
||||
},
|
||||
|
||||
getGroups(context, { offset, limit, search }) {
|
||||
search = typeof search === 'string' ? search : '';
|
||||
return api.get(OC.linkToOCS(`cloud/groups?offset=${offset}&limit=${limit}&search=${search}`, 2))
|
||||
.then((response) => {
|
||||
if (Object.keys(response.data.ocs.data.groups).length > 0) {
|
||||
response.data.ocs.data.groups.forEach(function(group) {
|
||||
context.commit('addGroup', {gid: group, displayName: group});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.catch((error) => context.commit('API_FAILURE', error));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all users with full details
|
||||
*
|
||||
|
@ -253,7 +268,7 @@ const actions = {
|
|||
addGroup(context, gid) {
|
||||
return api.requireAdmin().then((response) => {
|
||||
return api.post(OC.linkToOCS(`cloud/groups`, 2), {groupid: gid})
|
||||
.then((response) => context.commit('addGroup', gid))
|
||||
.then((response) => context.commit('addGroup', {gid: gid, displayName: gid}))
|
||||
.catch((error) => {throw error;});
|
||||
}).catch((error) => {
|
||||
context.commit('API_FAILURE', { gid, error });
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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 id="app">
|
||||
<app-navigation :menu="menu" />
|
||||
<div id="app-content" class="app-settings-content" :class="{ 'with-app-sidebar': currentApp, 'icon-loading': loadingList }">
|
||||
<app-list :category="category" :app="currentApp" :search="search"></app-list>
|
||||
<div id="app-sidebar" v-if="id && currentApp">
|
||||
<app-details :category="category" :app="currentApp"></app-details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import appNavigation from '../components/appNavigation';
|
||||
import appList from '../components/appList';
|
||||
import Vue from 'vue';
|
||||
import VueLocalStorage from 'vue-localstorage'
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import api from '../store/api';
|
||||
import AppDetails from '../components/appDetails';
|
||||
|
||||
Vue.use(VueLocalStorage)
|
||||
Vue.use(VueLocalStorage)
|
||||
|
||||
export default {
|
||||
name: 'Apps',
|
||||
props: {
|
||||
category: {
|
||||
type: String,
|
||||
default: 'installed',
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
AppDetails,
|
||||
appNavigation,
|
||||
appList,
|
||||
},
|
||||
methods: {
|
||||
setSearch(search) {
|
||||
this.search = search;
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.$store.dispatch('getCategories');
|
||||
this.$store.dispatch('getAllApps');
|
||||
this.$store.dispatch('getGroups', {offset: 0, limit: -1});
|
||||
this.$store.commit('setUpdateCount', this.$store.getters.getServerData.updateCount)
|
||||
},
|
||||
mounted() {
|
||||
// TODO: remove jQuery once we have a proper standardisation of the search
|
||||
$('#searchbox').show();
|
||||
let self = this;
|
||||
$('#searchbox').change(function(e) {
|
||||
self.setSearch($('#searchbox').val());
|
||||
});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
category: function (val, old) {
|
||||
this.setSearch('');
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return this.$store.getters.loading('categories');
|
||||
},
|
||||
loadingList() {
|
||||
return this.$store.getters.loading('list');
|
||||
},
|
||||
currentApp() {
|
||||
return this.apps.find(app => app.id === this.id );
|
||||
},
|
||||
categories() {
|
||||
return this.$store.getters.getCategories;
|
||||
},
|
||||
apps() {
|
||||
return this.$store.getters.getAllApps;
|
||||
},
|
||||
updateCount() {
|
||||
return this.$store.getters.getUpdateCount;
|
||||
},
|
||||
settings() {
|
||||
return this.$store.getters.getServerData;
|
||||
},
|
||||
|
||||
// BUILD APP NAVIGATION MENU OBJECT
|
||||
menu() {
|
||||
// Data provided php side
|
||||
let categories = this.$store.getters.getCategories;
|
||||
categories = Array.isArray(categories) ? categories : [];
|
||||
|
||||
// Map groups
|
||||
categories = categories.map(category => {
|
||||
let item = {};
|
||||
item.id = 'app-category-' + category.ident;
|
||||
item.icon = 'icon-category-' + category.ident;
|
||||
item.classes = []; // empty classes, active will be set later
|
||||
item.router = { // router link to
|
||||
name: 'apps-category',
|
||||
params: {category: category.ident}
|
||||
};
|
||||
item.text = category.displayName;
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
|
||||
// Add everyone group
|
||||
let defaultCategories = [
|
||||
{
|
||||
id: 'app-category-your-apps',
|
||||
classes: [],
|
||||
router: {name: 'apps'},
|
||||
icon: 'icon-category-installed',
|
||||
text: t('settings', 'Your apps'),
|
||||
},
|
||||
{
|
||||
id: 'app-category-enabled',
|
||||
classes: [],
|
||||
icon: 'icon-category-enabled',
|
||||
router: {name: 'apps-category', params: {category: 'enabled'}},
|
||||
text: t('settings', 'Active apps'),
|
||||
}, {
|
||||
id: 'app-category-disabled',
|
||||
classes: [],
|
||||
icon: 'icon-category-disabled',
|
||||
router: {name: 'apps-category', params: {category: 'disabled'}},
|
||||
text: t('settings', 'Disabled apps'),
|
||||
}
|
||||
];
|
||||
|
||||
if (!this.settings.appstoreEnabled) {
|
||||
return {
|
||||
id: 'appscategories',
|
||||
items: defaultCategories,
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$store.getters.getUpdateCount > 0) {
|
||||
defaultCategories.push({
|
||||
id: 'app-category-updates',
|
||||
classes: [],
|
||||
icon: 'icon-download',
|
||||
router: {name: 'apps-category', params: {category: 'updates'}},
|
||||
text: t('settings', 'Updates'),
|
||||
utils: {counter: this.$store.getters.getUpdateCount}
|
||||
});
|
||||
}
|
||||
|
||||
defaultCategories.push({
|
||||
id: 'app-category-app-bundles',
|
||||
classes: [],
|
||||
icon: 'icon-category-app-bundles',
|
||||
router: {name: 'apps-category', params: {category: 'app-bundles'}},
|
||||
text: t('settings', 'App bundles'),
|
||||
});
|
||||
|
||||
categories = defaultCategories.concat(categories);
|
||||
|
||||
// Set current group as active
|
||||
let activeGroup = categories.findIndex(group => group.id === 'app-category-' + this.category);
|
||||
if (activeGroup >= 0) {
|
||||
categories[activeGroup].classes.push('active');
|
||||
} else {
|
||||
categories[0].classes.push('active');
|
||||
}
|
||||
|
||||
categories.push({
|
||||
id: 'app-developer-docs',
|
||||
classes: [],
|
||||
href: this.settings.developerDocumentation,
|
||||
text: t('settings', 'Developer documentation') + ' ↗',
|
||||
});
|
||||
|
||||
// Return
|
||||
return {
|
||||
id: 'appscategories',
|
||||
items: categories,
|
||||
loading: this.loading
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,213 +0,0 @@
|
|||
<?php
|
||||
style('settings', 'settings');
|
||||
vendor_script(
|
||||
'core',
|
||||
[
|
||||
'marked/marked.min',
|
||||
]
|
||||
);
|
||||
script(
|
||||
'settings',
|
||||
[
|
||||
'settings',
|
||||
'apps',
|
||||
]
|
||||
);
|
||||
/** @var array $_ */
|
||||
/** @var \OCP\IURLGenerator $urlGenerator */
|
||||
$urlGenerator = $_['urlGenerator'];
|
||||
?>
|
||||
<script id="categories-template" type="text/x-handlebars-template">
|
||||
{{#each this}}
|
||||
<li id="app-category-{{ident}}" data-category-id="{{ident}}" tabindex="0">
|
||||
<a href="#" class="icon-category-{{ident}}">{{displayName}}</a>
|
||||
<div class="app-navigation-entry-utils">
|
||||
<ul>
|
||||
<li class="app-navigation-entry-utils-counter">{{ counter }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
||||
<?php if($_['appstoreEnabled']): ?>
|
||||
<li>
|
||||
<a class="app-external icon-info" target="_blank" rel="noreferrer noopener" href="<?php p($urlGenerator->linkToDocs('developer-manual')); ?>"><?php p($l->t('Developer documentation'));?> ↗</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
|
||||
<script id="app-template-installed" type="text/x-handlebars">
|
||||
{{#if newCategory}}
|
||||
<div class="apps-header">
|
||||
<div class="app-image"></div>
|
||||
<h2>{{categoryName}} <input class="enable" type="submit" data-bundleid="{{bundleId}}" data-active="true" value="<?php p($l->t('Enable all'));?>"/></h2>
|
||||
<div class="app-version"></div>
|
||||
<div class="app-level"></div>
|
||||
<div class="app-groups"></div>
|
||||
<div class="actions"> </div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="section" id="app-{{id}}">
|
||||
<div class="app-image app-image-icon"></div>
|
||||
<div class="app-name">
|
||||
{{#if detailpage}}
|
||||
<a href="{{detailpage}}" target="_blank" rel="noreferrer noopener">{{name}}</a>
|
||||
{{else}}
|
||||
{{name}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="app-version">{{version}}</div>
|
||||
<div class="app-level">
|
||||
{{{level}}}{{#unless internal}}<a href="https://apps.nextcloud.com/apps/{{id}}" target="_blank"><?php p($l->t('View in store'));?> ↗</a>{{/unless}}
|
||||
</div>
|
||||
|
||||
<div class="app-groups">
|
||||
{{#if active}}
|
||||
<div class="groups-enable">
|
||||
<input type="checkbox" class="groups-enable__checkbox checkbox" id="groups_enable-{{id}}"/>
|
||||
<label for="groups_enable-{{id}}"><?php p($l->t('Limit to groups')); ?></label>
|
||||
<input type="hidden" class="group_select" title="<?php p($l->t('All')); ?>">
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="warning hidden"></div>
|
||||
<input class="update hidden" type="submit" value="<?php p($l->t('Update to %s', array('{{update}}'))); ?>" data-appid="{{id}}" />
|
||||
{{#if canUnInstall}}
|
||||
<input class="uninstall" type="submit" value="<?php p($l->t('Remove')); ?>" data-appid="{{id}}" />
|
||||
{{/if}}
|
||||
{{#if active}}
|
||||
<input class="enable" type="submit" data-appid="{{id}}" data-active="true" value="<?php p($l->t("Disable"));?>"/>
|
||||
{{else}}
|
||||
<input class="enable{{#if needsDownload}} needs-download{{/if}}" type="submit" data-appid="{{id}}" data-active="false" {{#unless canInstall}}disabled="disabled"{{/unless}} value="<?php p($l->t("Enable"));?>"/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="app-template" type="text/x-handlebars">
|
||||
<div class="section" id="app-{{id}}">
|
||||
{{#if preview}}
|
||||
<div class="app-image{{#if previewAsIcon}} app-image-icon{{/if}} icon-loading">
|
||||
</div>
|
||||
{{/if}}
|
||||
<h2 class="app-name">
|
||||
{{#if detailpage}}
|
||||
<a href="{{detailpage}}" target="_blank" rel="noreferrer noopener">{{name}}</a>
|
||||
{{else}}
|
||||
{{name}}
|
||||
{{/if}}
|
||||
</h2>
|
||||
<div class="app-level">
|
||||
{{{level}}}
|
||||
</div>
|
||||
{{#if ratingNumThresholdReached }}
|
||||
<div class="app-score">{{{score}}}</div>
|
||||
{{/if}}
|
||||
<div class="app-detailpage"></div>
|
||||
|
||||
<div class="app-description-container hidden">
|
||||
<div class="app-version">{{version}}</div>
|
||||
{{#if profilepage}}<a href="{{profilepage}}" target="_blank" rel="noreferrer noopener">{{/if}}
|
||||
<div class="app-author"><?php p($l->t('by %s', ['{{author}}']));?>
|
||||
{{#if licence}}
|
||||
(<?php p($l->t('%s-licensed', ['{{licence}}'])); ?>)
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if profilepage}}</a>{{/if}}
|
||||
<div class="app-description">{{{description}}}</div>
|
||||
<!--<div class="app-changed">{{changed}}</div>-->
|
||||
{{#if documentation}}
|
||||
<p class="documentation">
|
||||
<?php p($l->t("Documentation:"));?>
|
||||
{{#if documentation.user}}
|
||||
<span class="userDocumentation">
|
||||
<a id="userDocumentation" class="appslink" href="{{documentation.user}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('User documentation'));?> ↗</a>
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if documentation.admin}}
|
||||
<span class="adminDocumentation">
|
||||
<a id="adminDocumentation" class="appslink" href="{{documentation.admin}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('Admin documentation'));?> ↗</a>
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if documentation.developer}}
|
||||
<span class="developerDocumentation">
|
||||
<a id="developerDocumentation" class="appslink" href="{{documentation.developer}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('Developer documentation'));?> ↗</a>
|
||||
</span>
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#if website}}
|
||||
<a id="userDocumentation" class="appslink" href="{{website}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('Visit website'));?> ↗</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if bugs}}
|
||||
<a id="adminDocumentation" class="appslink" href="{{bugs}}" target="_blank" rel="noreferrer noopener"><?php p($l->t('Report a bug'));?> ↗</a>
|
||||
{{/if}}
|
||||
</div><!-- end app-description-container -->
|
||||
<div class="app-description-toggle-show" role="link"><?php p($l->t("Show description …"));?></div>
|
||||
<div class="app-description-toggle-hide hidden" role="link"><?php p($l->t("Hide description …"));?></div>
|
||||
|
||||
<div class="app-dependencies update hidden">
|
||||
<p><?php p($l->t('This app has an update available.')); ?></p>
|
||||
</div>
|
||||
|
||||
{{#if missingMinOwnCloudVersion}}
|
||||
<div class="app-dependencies">
|
||||
<p><?php p($l->t('This app has no minimum Nextcloud version assigned. This will be an error in the future.')); ?></p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if missingMaxOwnCloudVersion}}
|
||||
<div class="app-dependencies">
|
||||
<p><?php p($l->t('This app has no maximum Nextcloud version assigned. This will be an error in the future.')); ?></p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#unless canInstall}}
|
||||
<div class="app-dependencies">
|
||||
<p><?php p($l->t('This app cannot be installed because the following dependencies are not fulfilled:')); ?></p>
|
||||
<ul class="missing-dependencies">
|
||||
{{#each missingDependencies}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<input class="update hidden" type="submit" value="<?php p($l->t('Update to %s', array('{{update}}'))); ?>" data-appid="{{id}}" />
|
||||
{{#if active}}
|
||||
<input class="enable" type="submit" data-appid="{{id}}" data-active="true" value="<?php p($l->t("Disable"));?>"/>
|
||||
<div class="groups-enable">
|
||||
<input type="checkbox" class="groups-enable__checkbox checkbox" id="groups_enable-{{id}}"/>
|
||||
<label for="groups_enable-{{id}}"><?php p($l->t('Enable only for specific groups')); ?></label>
|
||||
</div>
|
||||
<input type="hidden" class="group_select" title="<?php p($l->t('All')); ?>" style="width: 200px">
|
||||
{{else}}
|
||||
<input class="enable{{#if needsDownload}} needs-download{{/if}}" type="submit" data-appid="{{id}}" data-active="false" {{#unless canInstall}}disabled="disabled"{{/unless}} value="<?php p($l->t("Enable"));?>"/>
|
||||
{{/if}}
|
||||
{{#if canUnInstall}}
|
||||
<input class="uninstall" type="submit" value="<?php p($l->t('Remove')); ?>" data-appid="{{id}}" />
|
||||
{{/if}}
|
||||
|
||||
<div class="warning hidden"></div>
|
||||
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<div id="app-navigation" class="icon-loading" data-category="<?php p($_['category']);?>">
|
||||
<ul id="apps-categories">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div id="app-content" class="icon-loading">
|
||||
<div id="apps-list"></div>
|
||||
<div id="apps-list-empty" class="hidden emptycontent emptycontent-search">
|
||||
<div id="app-list-empty-icon" class="icon-search"></div>
|
||||
<h2><?php p($l->t('No apps found for your version')) ?></h2>
|
||||
</div>
|
||||
</div>
|
|
@ -1,266 +0,0 @@
|
|||
/**
|
||||
* ownCloud
|
||||
*
|
||||
* @author Vincent Petry
|
||||
* @copyright 2015 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version.
|
||||
*
|
||||
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
describe('OC.Settings.Apps tests', function() {
|
||||
var Apps;
|
||||
|
||||
beforeEach(function() {
|
||||
var $el = $('<div id="apps-list"></div>' +
|
||||
'<div id="apps-list-empty" class="hidden"></div>' +
|
||||
'<div id="app-template">' +
|
||||
// dummy template for testing
|
||||
'<div id="app-{{id}}" data-id="{{id}}" class="section">{{name}}</div>' +
|
||||
'</div>'
|
||||
);
|
||||
$('#testArea').append($el);
|
||||
|
||||
Apps = OC.Settings.Apps;
|
||||
});
|
||||
afterEach(function() {
|
||||
Apps.State.apps = null;
|
||||
Apps.State.currentCategory = null;
|
||||
});
|
||||
|
||||
describe('Filtering apps', function() {
|
||||
var oldApps;
|
||||
|
||||
function loadApps(appList) {
|
||||
Apps.State.apps = appList;
|
||||
|
||||
_.each(appList, function(appSpec) {
|
||||
Apps.renderApp(appSpec);
|
||||
});
|
||||
}
|
||||
|
||||
function getResultsFromDom() {
|
||||
var results = [];
|
||||
$('#apps-list .section:not(.hidden)').each(function() {
|
||||
results.push($(this).attr('data-id'));
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
loadApps([
|
||||
{id: 'appone', name: 'App One', description: 'The first app', author: 'author1', level: 200},
|
||||
{id: 'apptwo', name: 'App Two', description: 'The second app', author: 'author2', level: 100},
|
||||
{id: 'appthree', name: 'App Three', description: 'Third app', author: 'author3', level: 0},
|
||||
{id: 'somestuff', name: 'Some Stuff', description: 'whatever', author: 'author4', level: 0}
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no results when query does not match anything', function() {
|
||||
expect(getResultsFromDom().length).toEqual(4);
|
||||
expect($('#apps-list:not(.hidden)').length).toEqual(1);
|
||||
expect($('#apps-list-empty:not(.hidden)').length).toEqual(0);
|
||||
|
||||
Apps.filter('absurdity');
|
||||
expect(getResultsFromDom().length).toEqual(0);
|
||||
expect($('#apps-list:not(.hidden)').length).toEqual(0);
|
||||
expect($('#apps-list-empty:not(.hidden)').length).toEqual(1);
|
||||
|
||||
Apps.filter('');
|
||||
expect(getResultsFromDom().length).toEqual(4);
|
||||
expect($('#apps-list:not(.hidden)').length).toEqual(1);
|
||||
expect($('#apps-list-empty:not(.hidden)').length).toEqual(0);
|
||||
expect(getResultsFromDom().length).toEqual(4);
|
||||
});
|
||||
it('returns relevant results when query matches name', function() {
|
||||
expect($('#apps-list:not(.hidden)').length).toEqual(1);
|
||||
expect($('#apps-list-empty:not(.hidden)').length).toEqual(0);
|
||||
|
||||
var results;
|
||||
Apps.filter('app');
|
||||
results = getResultsFromDom();
|
||||
expect(results.length).toEqual(3);
|
||||
expect(results[0]).toEqual('appone');
|
||||
expect(results[1]).toEqual('apptwo');
|
||||
expect(results[2]).toEqual('appthree');
|
||||
|
||||
expect($('#apps-list:not(.hidden)').length).toEqual(1);
|
||||
expect($('#apps-list-empty:not(.hidden)').length).toEqual(0);
|
||||
});
|
||||
it('returns relevant result when query matches name', function() {
|
||||
var results;
|
||||
Apps.filter('TWO');
|
||||
results = getResultsFromDom();
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]).toEqual('apptwo');
|
||||
});
|
||||
it('returns relevant result when query matches description', function() {
|
||||
var results;
|
||||
Apps.filter('ever');
|
||||
results = getResultsFromDom();
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]).toEqual('somestuff');
|
||||
});
|
||||
it('returns relevant results when query matches author name', function() {
|
||||
var results;
|
||||
Apps.filter('author');
|
||||
results = getResultsFromDom();
|
||||
expect(results.length).toEqual(4);
|
||||
expect(results[0]).toEqual('appone');
|
||||
expect(results[1]).toEqual('apptwo');
|
||||
expect(results[2]).toEqual('appthree');
|
||||
expect(results[3]).toEqual('somestuff');
|
||||
});
|
||||
it('returns relevant result when query matches author name', function() {
|
||||
var results;
|
||||
Apps.filter('thor3');
|
||||
results = getResultsFromDom();
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]).toEqual('appthree');
|
||||
});
|
||||
it('returns relevant result when query matches level name', function() {
|
||||
var results;
|
||||
Apps.filter('Offic');
|
||||
results = getResultsFromDom();
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]).toEqual('appone');
|
||||
});
|
||||
it('returns relevant result when query matches level name', function() {
|
||||
var results;
|
||||
Apps.filter('Appro');
|
||||
results = getResultsFromDom();
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0]).toEqual('apptwo');
|
||||
});
|
||||
it('returns relevant result when query matches level name', function() {
|
||||
var results;
|
||||
Apps.filter('Exper');
|
||||
results = getResultsFromDom();
|
||||
expect(results.length).toEqual(2);
|
||||
expect(results[0]).toEqual('appthree');
|
||||
expect(results[1]).toEqual('somestuff');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading categories', function() {
|
||||
var suite = this;
|
||||
|
||||
beforeEach( function(){
|
||||
suite.server = sinon.fakeServer.create();
|
||||
});
|
||||
|
||||
afterEach( function(){
|
||||
suite.server.restore();
|
||||
});
|
||||
|
||||
function getResultsFromDom() {
|
||||
var results = [];
|
||||
$('#apps-list .section:not(.hidden)').each(function() {
|
||||
results.push($(this).attr('data-id'));
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
it('does not sort applications using the level', function() {
|
||||
Apps.loadCategory('TestId');
|
||||
|
||||
suite.server.requests[0].respond(
|
||||
200,
|
||||
{
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
JSON.stringify({
|
||||
apps: [
|
||||
{
|
||||
id: 'foo',
|
||||
name: 'Foo app',
|
||||
description: 'Hello',
|
||||
level: 0,
|
||||
author: 'foo'
|
||||
},
|
||||
{
|
||||
id: 'alpha',
|
||||
name: 'Alpha app',
|
||||
description: 'Hello',
|
||||
level: 300,
|
||||
author: ['alpha', 'beta']
|
||||
},
|
||||
{
|
||||
id: 'nolevel',
|
||||
name: 'No level',
|
||||
description: 'Hello',
|
||||
author: 'bar'
|
||||
},
|
||||
{
|
||||
id: 'zork',
|
||||
name: 'Some famous adventure game',
|
||||
description: 'Hello',
|
||||
level: 200,
|
||||
author: 'baz'
|
||||
|
||||
},
|
||||
{
|
||||
id: 'delta',
|
||||
name: 'Mathematical symbol',
|
||||
description: 'Hello',
|
||||
level: 200,
|
||||
author: 'foobar'
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
var results = getResultsFromDom();
|
||||
expect(results.length).toEqual(5);
|
||||
expect(results).toEqual(['alpha', 'foo', 'delta', 'nolevel', 'zork']);
|
||||
expect(OC.Settings.Apps.State.apps).toEqual({
|
||||
'foo': {
|
||||
id: 'foo',
|
||||
name: 'Foo app',
|
||||
description: 'Hello',
|
||||
level: 0,
|
||||
author: 'foo'
|
||||
},
|
||||
'alpha': {
|
||||
id: 'alpha',
|
||||
name: 'Alpha app',
|
||||
description: 'Hello',
|
||||
level: 300,
|
||||
author: ['alpha', 'beta']
|
||||
},
|
||||
'nolevel': {
|
||||
id: 'nolevel',
|
||||
name: 'No level',
|
||||
description: 'Hello',
|
||||
author: 'bar'
|
||||
},
|
||||
'zork': {
|
||||
id: 'zork',
|
||||
name: 'Some famous adventure game',
|
||||
description: 'Hello',
|
||||
level: 200,
|
||||
author: 'baz',
|
||||
},
|
||||
'delta': {
|
||||
id: 'delta',
|
||||
name: 'Mathematical symbol',
|
||||
description: 'Hello',
|
||||
level: 200,
|
||||
author: 'foobar'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -30,6 +30,7 @@ use OC\Settings\Controller\AppSettingsController;
|
|||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\ILogger;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\L10N\IFactory;
|
||||
use Test\TestCase;
|
||||
|
@ -69,6 +70,8 @@ class AppSettingsControllerTest extends TestCase {
|
|||
private $installer;
|
||||
/** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */
|
||||
private $urlGenerator;
|
||||
/** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
|
||||
private $logger;
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
@ -87,6 +90,7 @@ class AppSettingsControllerTest extends TestCase {
|
|||
$this->bundleFetcher = $this->createMock(BundleFetcher::class);
|
||||
$this->installer = $this->createMock(Installer::class);
|
||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->logger = $this->createMock(ILogger::class);
|
||||
|
||||
$this->appSettingsController = new AppSettingsController(
|
||||
'settings',
|
||||
|
@ -100,7 +104,8 @@ class AppSettingsControllerTest extends TestCase {
|
|||
$this->l10nFactory,
|
||||
$this->bundleFetcher,
|
||||
$this->installer,
|
||||
$this->urlGenerator
|
||||
$this->urlGenerator,
|
||||
$this->logger
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -109,32 +114,6 @@ class AppSettingsControllerTest extends TestCase {
|
|||
->method('isUpdateAvailable')
|
||||
->willReturn(false);
|
||||
$expected = new JSONResponse([
|
||||
[
|
||||
'id' => 2,
|
||||
'ident' => 'installed',
|
||||
'displayName' => 'Your apps',
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'ident' => 'updates',
|
||||
'displayName' => 'Updates',
|
||||
'counter' => 0,
|
||||
],
|
||||
[
|
||||
'id' => 0,
|
||||
'ident' => 'enabled',
|
||||
'displayName' => 'Enabled apps',
|
||||
],
|
||||
[
|
||||
'id' => 1,
|
||||
'ident' => 'disabled',
|
||||
'displayName' => 'Disabled apps',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'ident' => 'app-bundles',
|
||||
'displayName' => 'App bundles',
|
||||
],
|
||||
[
|
||||
'id' => 'auth',
|
||||
'ident' => 'auth',
|
||||
|
@ -196,6 +175,10 @@ class AppSettingsControllerTest extends TestCase {
|
|||
}
|
||||
|
||||
public function testViewApps() {
|
||||
$this->bundleFetcher->expects($this->once())->method('getBundles')->willReturn([]);
|
||||
$this->installer->expects($this->any())
|
||||
->method('isUpdateAvailable')
|
||||
->willReturn(false);
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getSystemValue')
|
||||
|
@ -210,11 +193,14 @@ class AppSettingsControllerTest extends TestCase {
|
|||
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
|
||||
|
||||
$expected = new TemplateResponse('settings',
|
||||
'apps',
|
||||
'settings',
|
||||
[
|
||||
'category' => 'installed',
|
||||
'serverData' => [
|
||||
'updateCount' => 0,
|
||||
'appstoreEnabled' => true,
|
||||
'urlGenerator' => $this->urlGenerator,
|
||||
'bundles' => [],
|
||||
'developerDocumentation' => ''
|
||||
]
|
||||
],
|
||||
'user');
|
||||
$expected->setContentSecurityPolicy($policy);
|
||||
|
@ -223,6 +209,10 @@ class AppSettingsControllerTest extends TestCase {
|
|||
}
|
||||
|
||||
public function testViewAppsAppstoreNotEnabled() {
|
||||
$this->installer->expects($this->any())
|
||||
->method('isUpdateAvailable')
|
||||
->willReturn(false);
|
||||
$this->bundleFetcher->expects($this->once())->method('getBundles')->willReturn([]);
|
||||
$this->config
|
||||
->expects($this->once())
|
||||
->method('getSystemValue')
|
||||
|
@ -237,11 +227,14 @@ class AppSettingsControllerTest extends TestCase {
|
|||
$policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com');
|
||||
|
||||
$expected = new TemplateResponse('settings',
|
||||
'apps',
|
||||
'settings',
|
||||
[
|
||||
'category' => 'installed',
|
||||
'serverData' => [
|
||||
'updateCount' => 0,
|
||||
'appstoreEnabled' => false,
|
||||
'urlGenerator' => $this->urlGenerator,
|
||||
'bundles' => [],
|
||||
'developerDocumentation' => ''
|
||||
]
|
||||
],
|
||||
'user');
|
||||
$expected->setContentSecurityPolicy($policy);
|
||||
|
|
Loading…
Reference in New Issue