From af12a33d9dd50859f39e7e3599e5ab9ad13dbd48 Mon Sep 17 00:00:00 2001 From: Yuriy Bakhtin Date: Fri, 4 Feb 2022 01:16:37 +0300 Subject: [PATCH] Rework modules section (#5476) * Rework modules section * Rework modules section * Filter modules * Menu controls for module cards * Display only modules * Filter online modules by category * Filter installed modules by category * Filter modules by tags * Display module status * Reorder tags on module filters * Display HumHub update info on modules list * Menu controls on online modules * Display available updates for modules * Remove not used code of old module views * Link to settings from modules page * Redesign modules for user and space * Fix user modules layout on fluid themes * Animate module updating * Button to update all modules * Update style for modules list on space creating * Fixed errors without available Marketplace module * Sort enabled modules before disabled * Use MarketplaceModule::$enabled on init modules list * Update method getEnabledMarketplaceModule() * Use MarketplaceModule::$enabled on init modules list * Move module online info methods into separate behavior * Update code of modules list * Rename 'humhub.directory.js' to 'humhub.cards.js' * Factorize online module data loading * Move purchase licence key to new modules layout * Align center update info * Fix filter modules * Fix filter modules by tags "Installed"/"Not Installed" * Update styles of modules settings icon * New online module status "isPartner" * Keep single active filter tag on modules list * Hide the not installed deprecated modules * Update layout of module status label * Space above filter "Tags" * Detect module status "Professional Edition" as top priority * Hide installed or not installed modules on single filter tag * Update styles for upgrade humhub info on modules list * Tooltip for button "Professional Edition" * Fix "Buy" link * Restyle no modules results * Update no modules results * Minor changes * Changed Updater link Co-authored-by: Lucas Bartholemy Co-authored-by: Lucas Bartholemy --- CHANGELOG_DEV.md | 3 +- .../{DirectoryAsset.php => CardsAsset.php} | 4 +- protected/humhub/components/Module.php | 24 +- protected/humhub/components/ModuleManager.php | 106 +- protected/humhub/components/OnlineModule.php | 142 + protected/humhub/config/assets-prod.php | 10 +- .../modules/admin/assets/ModuleAsset.php | 26 + .../admin/controllers/ModuleController.php | 55 +- .../modules/admin/events/ModulesEvent.php | 25 + .../modules/admin/resources/css/modules.css | 1 + .../admin/resources/css/modules.css.map | 1 + .../modules/admin/resources/css/modules.less | 96 + .../modules/admin/views/layouts/module.php | 25 +- .../modules/admin/views/module/list.php | 98 +- .../modules/admin/widgets/AdminMenu.php | 3 +- .../admin/widgets/ModuleActionButtons.php | 70 + .../modules/admin/widgets/ModuleCard.php | 66 + .../modules/admin/widgets/ModuleControls.php | 164 + .../modules/admin/widgets/ModuleFilters.php | 45 + .../modules/admin/widgets/ModuleStatus.php | 105 + .../humhub/modules/admin/widgets/Modules.php | 107 + .../admin/widgets/views/moduleCard.php | 35 + .../admin/widgets/views/moduleControls.php | 31 + .../admin/widgets/views/moduleGroup.php | 42 + .../ContentContainerModuleManager.php | 2 +- .../content/widgets/ModuleActionButtons.php | 108 + .../modules/content/widgets/ModuleCard.php | 40 + .../content/widgets/views/moduleCard.php | 32 + .../file/handler/FileHandlerCollection.php | 6 +- .../humhub/modules/marketplace/Events.php | 339 +- .../humhub/modules/marketplace/Module.php | 21 +- .../modules/marketplace/assets/Assets.php | 32 + .../components/OnlineModuleManager.php | 44 +- .../humhub/modules/marketplace/config.php | 11 +- .../controllers/BrowseController.php | 61 +- .../controllers/PurchaseController.php | 62 +- .../controllers/UpdateController.php | 46 +- .../modules/marketplace/models/Module.php | 184 + .../marketplace/resources/css/modules.css | 1 + .../marketplace/resources/css/modules.css.map | 1 + .../marketplace/resources/css/modules.less | 89 + .../resources/js/humhub.marketplace.js | 134 + .../marketplace/views/browse/_module.php | 105 - .../modules/marketplace/views/browse/list.php | 62 - .../marketplace/views/purchase/list.php | 144 +- .../modules/marketplace/views/update/list.php | 67 - .../widgets/ModuleInstallActionButtons.php | 82 + .../widgets/ModuleUpdateActionButtons.php | 56 + .../widgets/views/moduleInstallCard.php | 34 + .../widgets/views/moduleUpdateCard.php | 33 + .../widgets/views/moduleUpdateInfo.php | 23 + .../space/controllers/SpacesController.php | 2 +- .../manage/controllers/ModuleController.php | 5 +- .../modules/manage/views/module/index.php | 80 +- .../modules/space/views/create/moduleCard.php | 31 + .../modules/space/views/create/modules.php | 62 +- .../modules/space/views/spaces/_layout.php | 2 +- .../modules/space/views/spaces/index.php | 6 +- .../space/widgets/ModuleActionButtons.php | 72 + .../tour/widgets/views/guide_profile.php | 2 +- .../modules/tour/widgets/views/tourPanel.php | 2 +- .../modules/ui/widgets/DirectoryFilters.php | 46 +- .../ui/widgets/views/directoryFilter.php | 4 +- .../ui/widgets/views/directoryFilters.php | 7 +- .../user/controllers/AccountController.php | 11 +- .../user/controllers/PeopleController.php | 2 +- .../user/views/account/_userModulesLayout.php | 23 + .../user/views/account/editModules.php | 71 +- .../modules/user/views/people/_layout.php | 2 +- .../modules/user/views/people/index.php | 6 +- .../modules/user/widgets/PeopleFilters.php | 2 +- static/js/humhub/humhub.cards.js | 124 + static/js/humhub/humhub.directory.js | 92 - static/less/button.less | 6 + static/less/{directory.less => cards.less} | 90 +- static/less/humhub.less | 7 +- static/less/module.less | 8 - themes/HumHub/css/theme.css | 5486 +---------------- 78 files changed, 2949 insertions(+), 6405 deletions(-) rename protected/humhub/assets/{DirectoryAsset.php => CardsAsset.php} (81%) create mode 100644 protected/humhub/components/OnlineModule.php create mode 100644 protected/humhub/modules/admin/assets/ModuleAsset.php create mode 100644 protected/humhub/modules/admin/events/ModulesEvent.php create mode 100644 protected/humhub/modules/admin/resources/css/modules.css create mode 100644 protected/humhub/modules/admin/resources/css/modules.css.map create mode 100644 protected/humhub/modules/admin/resources/css/modules.less create mode 100644 protected/humhub/modules/admin/widgets/ModuleActionButtons.php create mode 100644 protected/humhub/modules/admin/widgets/ModuleCard.php create mode 100644 protected/humhub/modules/admin/widgets/ModuleControls.php create mode 100644 protected/humhub/modules/admin/widgets/ModuleFilters.php create mode 100644 protected/humhub/modules/admin/widgets/ModuleStatus.php create mode 100644 protected/humhub/modules/admin/widgets/Modules.php create mode 100644 protected/humhub/modules/admin/widgets/views/moduleCard.php create mode 100644 protected/humhub/modules/admin/widgets/views/moduleControls.php create mode 100644 protected/humhub/modules/admin/widgets/views/moduleGroup.php create mode 100644 protected/humhub/modules/content/widgets/ModuleActionButtons.php create mode 100644 protected/humhub/modules/content/widgets/ModuleCard.php create mode 100644 protected/humhub/modules/content/widgets/views/moduleCard.php create mode 100644 protected/humhub/modules/marketplace/assets/Assets.php create mode 100644 protected/humhub/modules/marketplace/models/Module.php create mode 100644 protected/humhub/modules/marketplace/resources/css/modules.css create mode 100644 protected/humhub/modules/marketplace/resources/css/modules.css.map create mode 100644 protected/humhub/modules/marketplace/resources/css/modules.less create mode 100644 protected/humhub/modules/marketplace/resources/js/humhub.marketplace.js delete mode 100644 protected/humhub/modules/marketplace/views/browse/_module.php delete mode 100644 protected/humhub/modules/marketplace/views/browse/list.php delete mode 100644 protected/humhub/modules/marketplace/views/update/list.php create mode 100644 protected/humhub/modules/marketplace/widgets/ModuleInstallActionButtons.php create mode 100644 protected/humhub/modules/marketplace/widgets/ModuleUpdateActionButtons.php create mode 100644 protected/humhub/modules/marketplace/widgets/views/moduleInstallCard.php create mode 100644 protected/humhub/modules/marketplace/widgets/views/moduleUpdateCard.php create mode 100644 protected/humhub/modules/marketplace/widgets/views/moduleUpdateInfo.php create mode 100644 protected/humhub/modules/space/views/create/moduleCard.php create mode 100644 protected/humhub/modules/space/widgets/ModuleActionButtons.php create mode 100644 protected/humhub/modules/user/views/account/_userModulesLayout.php create mode 100644 static/js/humhub/humhub.cards.js delete mode 100644 static/js/humhub/humhub.directory.js rename static/less/{directory.less => cards.less} (65%) delete mode 100644 static/less/module.less diff --git a/CHANGELOG_DEV.md b/CHANGELOG_DEV.md index aa6585725b..529b7e0440 100644 --- a/CHANGELOG_DEV.md +++ b/CHANGELOG_DEV.md @@ -2,10 +2,11 @@ ------------------------------------------ - Fix #5434: Hide disabled next/prev buttons on guide first/last steps - Fix #5456: `canImpersonate` only possible for SystemAdmins +- Enh #5476: Rework modules administration section - Enh #5472: New interface `TabbedFormModel` for activate first tab with error input - Enh #5224: Add reply-to email in the settings - Enh #5471: On the pending approval page, add grouped actions and custom columns - Enh #5490: Display confirmation message before display embedded content - Enh #5258: Display who invited the user on the Approval page - Enh #4890: Allow to define actions in a controller which should not be intercepted by other actions -- Enh #5510: oEmbed support for other social networks, redesign of oEmbed settings pages \ No newline at end of file +- Enh #5510: oEmbed support for other social networks, redesign of oEmbed settings pages diff --git a/protected/humhub/assets/DirectoryAsset.php b/protected/humhub/assets/CardsAsset.php similarity index 81% rename from protected/humhub/assets/DirectoryAsset.php rename to protected/humhub/assets/CardsAsset.php index 51073df0f6..9661530c6c 100644 --- a/protected/humhub/assets/DirectoryAsset.php +++ b/protected/humhub/assets/CardsAsset.php @@ -10,13 +10,13 @@ namespace humhub\assets; use humhub\components\assets\WebStaticAssetBundle; use yii\web\View; -class DirectoryAsset extends WebStaticAssetBundle +class CardsAsset extends WebStaticAssetBundle { /** * @inheritdoc */ public $js = [ - 'js/humhub/humhub.directory.js', + 'js/humhub/humhub.cards.js', ]; /** diff --git a/protected/humhub/components/Module.php b/protected/humhub/components/Module.php index aeadbd2be1..aecf4f189a 100644 --- a/protected/humhub/components/Module.php +++ b/protected/humhub/components/Module.php @@ -8,21 +8,23 @@ namespace humhub\components; -use humhub\modules\activity\components\BaseActivity; -use humhub\modules\activity\models\Activity; -use Yii; -use yii\helpers\Json; use humhub\models\Setting; +use humhub\modules\activity\components\BaseActivity; +use humhub\modules\content\models\ContentContainerSetting; use humhub\modules\file\libs\FileHelper; use humhub\modules\notification\components\BaseNotification; -use humhub\modules\content\models\ContentContainerSetting; +use Yii; +use yii\helpers\Json; use yii\web\AssetBundle; -use yii\web\HttpException; /** * Base Class for Modules / Extensions * + * @property-read string $name + * @property-read string $description + * @property-read bool $isActivated * @property SettingsManager $settings + * @mixin OnlineModule * @author luke */ class Module extends \yii\base\Module @@ -204,6 +206,16 @@ class Module extends \yii\base\Module return $this->getBasePath() . '/' . $this->resourcesPath; } + /** + * Check this module is activated + * + * @return bool + */ + public function getIsActivated(): bool + { + return (bool) Yii::$app->hasModule($this->id); + } + /** * Enables this module * diff --git a/protected/humhub/components/ModuleManager.php b/protected/humhub/components/ModuleManager.php index c05a92ec37..4657477d91 100644 --- a/protected/humhub/components/ModuleManager.php +++ b/protected/humhub/components/ModuleManager.php @@ -12,6 +12,8 @@ use humhub\components\bootstrap\ModuleAutoLoader; use humhub\components\console\Application as ConsoleApplication; use humhub\libs\BaseSettingsManager; use humhub\models\ModuleEnabled; +use humhub\modules\admin\events\ModulesEvent; +use humhub\modules\marketplace\Module as ModuleMarketplace; use Yii; use yii\base\Component; use yii\base\Event; @@ -51,6 +53,12 @@ class ModuleManager extends Component */ const EVENT_AFTER_MODULE_DISABLE = 'afterModuleDisabled'; + /** + * @event triggered after filter modules + * @since 1.11 + */ + const EVENT_AFTER_FILTER_MODULES = 'afterFilterModules'; + /** * Create a backup on module folder deletion * @@ -239,25 +247,25 @@ class ModuleManager extends Component */ public function getModules($options = []) { + $options = array_merge([ + 'includeCoreModules' => false, + 'enabled' => false, + 'returnClass' => false, + ], $options); + $modules = []; - foreach ($this->modules as $id => $class) { - - // Skip core modules - if (!isset($options['includeCoreModules']) || $options['includeCoreModules'] === false) { - if (in_array($class, $this->coreModules)) { - continue; - } + if (!$options['includeCoreModules'] && in_array($class, $this->coreModules)) { + // Skip core modules + continue; } - - if (isset($options['enabled']) && $options['enabled'] === true) { - if (!in_array($class, $this->coreModules) && !in_array($id, $this->enabledModules)) { - continue; - } + if ($options['enabled'] && !in_array($class, $this->coreModules) && !in_array($id, $this->enabledModules)) { + // Skip disabled modules + continue; } - if (isset($options['returnClass']) && $options['returnClass']) { + if ($options['returnClass']) { $modules[$id] = $class; } else { $module = $this->getModule($id); @@ -270,6 +278,70 @@ class ModuleManager extends Component return $modules; } + /** + * Filter modules by keyword and by additional filters from module event + * + * @param Module[] $modules + * @param array $filters + * @return Module[] + */ + public function filterModules(array $modules, $filters = []): array + { + $filters = array_merge([ + 'keyword' => null, + ], $filters); + + $modules = $this->filterModulesByKeyword($modules, $filters['keyword']); + + $modulesEvent = new ModulesEvent(['modules' => $modules]); + $this->trigger(static::EVENT_AFTER_FILTER_MODULES, $modulesEvent); + + return $modulesEvent->modules; + } + + /** + * Filter modules by keyword + * + * @param Module[] $modules + * @param null|string $keyword + * @return Module[] + */ + public function filterModulesByKeyword(array $modules, $keyword = null): array + { + if ($keyword === null) { + $keyword = Yii::$app->request->get('keyword', ''); + } + + if (!is_scalar($keyword) || $keyword === '') { + return $modules; + } + + foreach ($modules as $id => $module) { + /* @var Module $module */ + $searchFields = [$id]; + if (isset($module->name)) { + $searchFields[] = $module->name; + } + if (isset($module->description)) { + $searchFields[] = $module->description; + } + + $keywordFound = false; + foreach ($searchFields as $searchField) { + if (stripos($searchField, $keyword) !== false) { + $keywordFound = true; + continue; + } + } + + if (!$keywordFound) { + unset($modules[$id]); + } + } + + return $modules; + } + /** * Returns all enabled modules and supportes further options as [[getModules()]]. * @@ -359,8 +431,12 @@ class ModuleManager extends Component } // Check is in dynamic/marketplace module folder - if (strpos($module->getBasePath(), Yii::getAlias(Yii::$app->getModule('marketplace')->modulesPath)) !== false) { - return true; + /** @var ModuleMarketplace $marketplaceModule */ + $marketplaceModule = Yii::$app->getModule('marketplace'); + if ($marketplaceModule !== null) { + if (strpos($module->getBasePath(), Yii::getAlias($marketplaceModule->modulesPath)) !== false) { + return true; + } } return false; diff --git a/protected/humhub/components/OnlineModule.php b/protected/humhub/components/OnlineModule.php new file mode 100644 index 0000000000..be7919425e --- /dev/null +++ b/protected/humhub/components/OnlineModule.php @@ -0,0 +1,142 @@ + + * @since 1.11 + */ +class OnlineModule extends Component +{ + /** + * @var Module + */ + public $module; + + /** + * @var array the cached info loaded from online + */ + private $_onlineInfo = null; + + /** + * Get online info of the Module + * + * @param string|null $field Null - to return all fields, String - to return a value of the requested field: + * - id + * - name + * - description + * - useCases + * - featured + * - showDisclaimer + * - isThirdParty + * - isCommunity + * - isPartner + * - isDeprecated + * - latestVersion + * - moduleImageUrl + * - marketplaceUrl + * - latestCompatibleVersion + * - purchased + * - price_eur + * - price_request_quote + * - checkoutUrl + * - professional_only + * - categories + * @return array|null|string + */ + public function info(?string $field = null) + { + if ($this->_onlineInfo === null) { + /* @var MarketplaceModule $marketplaceModule */ + $marketplaceModule = Yii::$app->getModule('marketplace'); + if (!($marketplaceModule instanceof MarketplaceModule && $marketplaceModule->enabled)) { + return null; + } + + if ($this->module instanceof ModelModule) { + $this->_onlineInfo = (array)$this->module; + } else { + $onlineModules = $marketplaceModule->onlineModuleManager->getModules(); + $this->_onlineInfo = isset($onlineModules[$this->module->id]) ? $onlineModules[$this->module->id] : []; + } + } + + if ($field === null) { + return $this->_onlineInfo; + } + + return $this->_onlineInfo[$field] ?? null; + } + + public function getIsInstalled(): bool + { + return Yii::$app->moduleManager->hasModule($this->module->id); + } + + public function isProOnly(): bool + { + if (empty($this->info('professional_only'))) { + return false; + } + + /* @var MarketplaceModule */ + $marketplaceModule = Yii::$app->getModule('marketplace'); + if (!($marketplaceModule instanceof MarketplaceModule && $marketplaceModule->enabled)) { + return false; + } + + return $marketplaceModule->licence->type !== Licence::LICENCE_TYPE_PRO; + } + + public function getIsProOnly(): bool + { + return $this->isProOnly(); + } + + public function getCategories(): array + { + $onlineInfo = $this->info(); + return $onlineInfo['categories'] ?? []; + } + + public function getIsFeatured(): bool + { + return (bool) $this->info('featured'); + } + + public function getIsThirdParty(): bool + { + return (bool) $this->info('isThirdParty'); + } + + public function getIsPartner(): bool + { + return (bool) $this->info('isPartner'); + } + + public function getIsDeprecated(): bool + { + return (bool) $this->info('isDeprecated'); + } +} diff --git a/protected/humhub/config/assets-prod.php b/protected/humhub/config/assets-prod.php index f63740d1d1..c500ae9bb4 100644 --- a/protected/humhub/config/assets-prod.php +++ b/protected/humhub/config/assets-prod.php @@ -2,7 +2,7 @@ /** * This file is generated by the "yii asset" command. * DO NOT MODIFY THIS FILE DIRECTLY. - * @version 2021-09-16 07:53:30 + * @version 2021-12-22 09:17:29 */ return [ 'app' => [ @@ -393,6 +393,14 @@ return [ 'defer', ], ], + 'humhub\\assets\\JqueryCookieAsset' => [ + 'sourcePath' => null, + 'js' => [], + 'css' => [], + 'depends' => [ + 'defer', + ], + ], 'humhub\\modules\\user\\assets\\UserAsset' => [ 'sourcePath' => null, 'js' => [], diff --git a/protected/humhub/modules/admin/assets/ModuleAsset.php b/protected/humhub/modules/admin/assets/ModuleAsset.php new file mode 100644 index 0000000000..22abf5065d --- /dev/null +++ b/protected/humhub/modules/admin/assets/ModuleAsset.php @@ -0,0 +1,26 @@ +appendPageTitle(Yii::t('AdminModule.base', 'Modules')); - $this->subLayout = '@admin/views/layouts/module'; return parent::init(); } @@ -49,7 +54,7 @@ class ModuleController extends Controller public function getAccessRules() { return [ - ['permissions' => \humhub\modules\admin\permissions\ManageModules::class] + ['permissions' => ManageModules::class] ]; } @@ -59,53 +64,9 @@ class ModuleController extends Controller return $this->redirect(['/admin/module/list']); } - public function actionList() { - $installedModules = Yii::$app->moduleManager->getModules(); - - return $this->render('list', [ - 'installedModules' => $installedModules, - 'deprecatedModuleIds' => $this->getDeprecatedModules(), - 'marketplaceUrls' => $this->getMarketplaceUrls() - ]); - } - - - private function getMarketplaceUrls() - { - $marketplaceUrls = []; - - if (Yii::$app->hasModule('marketplace')) { - try { - foreach (Yii::$app->getModule('marketplace')->onlineModuleManager->getModules() as $id => $module) { - if (!empty($module['marketplaceUrl'])) { - $marketplaceUrls[$id] = $module['marketplaceUrl']; - } - } - } catch (\Exception $ex) { - } - } - - return $marketplaceUrls; - } - - - private function getDeprecatedModules() - { - $deprecatedModuleIds = []; - if (Yii::$app->hasModule('marketplace')) { - try { - foreach (Yii::$app->getModule('marketplace')->onlineModuleManager->getModules() as $id => $module) { - if (!empty($module['isDeprecated'])) { - $deprecatedModuleIds[] = $id; - } - } - } catch (\Exception $ex) { - } - } - - return $deprecatedModuleIds; + return $this->render('list'); } /** diff --git a/protected/humhub/modules/admin/events/ModulesEvent.php b/protected/humhub/modules/admin/events/ModulesEvent.php new file mode 100644 index 0000000000..4b0c675640 --- /dev/null +++ b/protected/humhub/modules/admin/events/ModulesEvent.php @@ -0,0 +1,25 @@ +div{padding-bottom:8px}.container-modules .card-module .card-body>div:last-child{padding-bottom:0}.container-modules .card-module .card-title{color:#000}.container-modules .card-module .card-footer{padding-bottom:14px}.container-modules .card-module .card-footer a.btn{float:none}.container-modules .card-module .card-footer.text-right a.btn{margin-left:8px;margin-right:0}.container-modules .card-module .card-footer.text-right a.btn:first-child{margin-left:0}.container-modules .module-settings-link{float:right;color:#02A1B1;font-size:22px;line-height:20px;margin:2px}.container-content-modules{width:100%;padding:0 18px 5px 5px}.container-content-modules h4{font-size:16px;color:#000}.container-content-modules .card{width:100%;padding-right:3px}.container-content-modules .card .card-panel{margin-top:3px}@media (min-width:460px){.container-content-modules .card{width:50%}}@media (min-width:656px){.container-content-modules .card{width:33.33333333%}}@media (min-width:768px){.container-content-modules{padding:0 12px 5px 0}}@media (min-width:1200px){.container-content-modules .card{width:25%}}.container-create-space-modules{width:100%;padding:0}.container-create-space-modules .card .card-panel{background:#F5F5F5}/*# sourceMappingURL=modules.css.map */ \ No newline at end of file diff --git a/protected/humhub/modules/admin/resources/css/modules.css.map b/protected/humhub/modules/admin/resources/css/modules.css.map new file mode 100644 index 0000000000..ed4ba3aa60 --- /dev/null +++ b/protected/humhub/modules/admin/resources/css/modules.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["modules.less"],"names":[],"mappings":"AAAA,kBACI,eACI,cAAA,CACA,gBAAA,CACA,UAAA,CACA,mBALR,kBAOI,KAAI,OACA,gBARR,kBAUI,aACI,cACI,kBAZZ,kBAUI,aAII,YACI,eAAA,CACA,cAAA,CACA,cAjBZ,kBAUI,aAII,WAII,KACI,mBACA,kBAVZ,aAII,WAII,IAEK,YACG,iBArBpB,kBAUI,aAeI,aACI,WA1BZ,kBAUI,aAkBI,cACI,oBA7BZ,kBAUI,aAkBI,aAEI,EAAC,KACG,WAEJ,kBAvBR,aAkBI,aAKK,WACG,EAAC,KACG,eAAA,CACA,eACA,kBA3BhB,aAkBI,aAKK,WACG,EAAC,IAGI,aACG,cAtCxB,kBA4CI,uBACI,WAAA,CACA,aAAA,CACA,cAAA,CACA,gBAAA,CACA,WAIR,2BACI,UAAA,CACA,uBAFJ,0BAGI,IACI,cAAA,CACA,WALR,0BAOI,OACI,UAAA,CACA,kBATR,0BAOI,MAGI,aACI,eAGR,QAA0B,iBAA1B,0BACI,OACI,WAGR,QAA0B,iBAA1B,0BACI,OACI,oBAGR,QAA0B,iBAA1B,2BACI,sBAEJ,QAA2B,kBAA3B,0BACI,OACI,WAKZ,gCACI,UAAA,CACA,UAFJ,+BAGI,MACI,aACI","file":"modules.css"} \ No newline at end of file diff --git a/protected/humhub/modules/admin/resources/css/modules.less b/protected/humhub/modules/admin/resources/css/modules.less new file mode 100644 index 0000000000..0b03bd5106 --- /dev/null +++ b/protected/humhub/modules/admin/resources/css/modules.less @@ -0,0 +1,96 @@ +.container-modules { + .modules-type { + font-size: 16px; + font-weight: bold; + color: #000; + margin: 70px 0 40px; + } + .row.cards { + margin-top: 40px; + } + .card-module { + .card-header { + position: relative; + } + .card-body { + padding-top: 8px; + font-size: 13px; + color: #6C787E; + > div { + padding-bottom: 8px; + &:last-child { + padding-bottom: 0; + } + } + } + .card-title { + color: #000; + } + .card-footer { + padding-bottom: 14px; + a.btn { + float: none; + } + &.text-right { + a.btn { + margin-left: 8px; + margin-right: 0; + &:first-child { + margin-left: 0; + } + } + } + } + } + .module-settings-link { + float: right; + color: #02A1B1; + font-size: 22px; + line-height: 20px; + margin: 2px; + } +} + +.container-content-modules { + width: 100%; + padding: 0 18px 5px 5px; + h4 { + font-size: 16px; + color: #000; + } + .card { + width: 100%; + padding-right: 3px; + .card-panel { + margin-top: 3px; + } + } + @media (min-width: 460px) { + .card { + width: 50%; + } + } + @media (min-width: 656px) { + .card { + width: 33.33333333%; + } + } + @media (min-width: 768px) { + padding: 0 12px 5px 0; + } + @media (min-width: 1200px) { + .card { + width: 25%; + } + } +} + +.container-create-space-modules { + width: 100%; + padding: 0; + .card { + .card-panel { + background: #F5F5F5; + } + } +} \ No newline at end of file diff --git a/protected/humhub/modules/admin/views/layouts/module.php b/protected/humhub/modules/admin/views/layouts/module.php index 68eda2f72b..2c043d2598 100644 --- a/protected/humhub/modules/admin/views/layouts/module.php +++ b/protected/humhub/modules/admin/views/layouts/module.php @@ -1,15 +1,18 @@ -beginContent('@admin/views/layouts/main.php') ?> -
-
- Module administration'); ?> + - +use humhub\modules\ui\view\helpers\ThemeHelper; + +/* @var $content string */ +?> +
+
+
+
-
- - -
-endContent(); ?> diff --git a/protected/humhub/modules/admin/views/module/list.php b/protected/humhub/modules/admin/views/module/list.php index 50a4d2f5e8..54c9619806 100644 --- a/protected/humhub/modules/admin/views/module/list.php +++ b/protected/humhub/modules/admin/views/module/list.php @@ -1,78 +1,30 @@ - -
- -
-
- - - $module) : ?> -
- 64x64 - -
-

getName(); ?> - - hasModule($module->id)) : ?> - - - - id, $deprecatedModuleIds)): ?> - - - -

- -

getDescription(); ?>

- -
- - getVersion(); ?> - - hasModule($module->id)) : ?> - getConfigUrl() != "") : ?> - · getConfigUrl(), ['style' => 'font-weight:bold']); ?> - - - - · $moduleId]), ['data-target' => '#globalModal']); ?> - - - · $moduleId]), ['data-method' => 'POST', 'data-confirm' => Yii::t('AdminModule.modules', 'Are you sure? *ALL* module data will be lost!')]); ?> - - - · $moduleId]), ['data-method' => 'POST', 'style' => 'font-weight:bold', 'data-loader' => "modal", 'data-message' => Yii::t('AdminModule.modules', 'Enable module...')]); ?> - - - moduleManager->canRemoveModule($moduleId)): ?> - · $moduleId]), ['data-method' => 'POST', 'data-confirm' => Yii::t('AdminModule.modules', 'Are you sure? *ALL* module related data and files will be lost!')]); ?> - - - - · ' - , $marketplaceUrls[$moduleId], - ['rel' => 'noopener', 'target' => '_blank'] - ); ?> - - · $moduleId]), ['data-target' => '#globalModal']); ?> - -
- -
-
-
- +
+
+ Module Administration'); ?> +
+
+ +
+ + diff --git a/protected/humhub/modules/admin/widgets/AdminMenu.php b/protected/humhub/modules/admin/widgets/AdminMenu.php index 91d6a29554..4ad0741bac 100644 --- a/protected/humhub/modules/admin/widgets/AdminMenu.php +++ b/protected/humhub/modules/admin/widgets/AdminMenu.php @@ -178,8 +178,7 @@ class AdminMenu extends LeftNavigation { /** @var Module $module */ $module = Yii::$app->getModule('marketplace'); - - if (!$module->enabled) { + if ($module === null || !$module->enabled) { return ''; } diff --git a/protected/humhub/modules/admin/widgets/ModuleActionButtons.php b/protected/humhub/modules/admin/widgets/ModuleActionButtons.php new file mode 100644 index 0000000000..08fe2a28d1 --- /dev/null +++ b/protected/humhub/modules/admin/widgets/ModuleActionButtons.php @@ -0,0 +1,70 @@ +{buttons}
'; + + /** + * @inheritdoc + */ + public function run() + { + $html = ''; + + if ($this->module->isActivated) { + if ($this->module->getConfigUrl() != '') { + $html .= Button::asLink(Yii::t('AdminModule.modules', 'Configure'), $this->module->getConfigUrl()) + ->cssClass('btn btn-sm btn-info'); + } + $html .= Button::asLink('  ' . Yii::t('AdminModule.modules', 'Activated'), Url::to(['/admin/module/disable', 'moduleId' => $this->module->id])) + ->cssClass('btn btn-sm btn-info active') + ->options([ + 'data-method' => 'POST', + 'data-confirm' => Yii::t('AdminModule.modules', 'Are you sure? *ALL* module data will be lost!') + ]); + } else { + $html .= Button::asLink(Yii::t('AdminModule.modules', 'Activate'), Url::to(['/admin/module/enable', 'moduleId' => $this->module->id])) + ->cssClass('btn btn-sm btn-info') + ->options([ + 'data-method' => 'POST', + 'data-loader' => 'modal', + 'data-message' => Yii::t('AdminModule.modules', 'Enable module...') + ]); + } + + if (trim($html) === '') { + return ''; + } + + return str_replace('{buttons}', $html, $this->template); + } + +} diff --git a/protected/humhub/modules/admin/widgets/ModuleCard.php b/protected/humhub/modules/admin/widgets/ModuleCard.php new file mode 100644 index 0000000000..5ec76c859a --- /dev/null +++ b/protected/humhub/modules/admin/widgets/ModuleCard.php @@ -0,0 +1,66 @@ +template)) { + $this->template = '
{card}
'; + } + + if (empty($this->view)) { + $this->view = 'moduleCard'; + } + } + + /** + * @inheritdoc + */ + public function run() + { + $onlineModule = new OnlineModule(['module' => $this->module]); + + $card = $this->render($this->view, [ + 'module' => $this->module, + 'isFeaturedModule' => $onlineModule->isFeatured, + ]); + + return str_replace('{card}', $card, $this->template); + } + +} diff --git a/protected/humhub/modules/admin/widgets/ModuleControls.php b/protected/humhub/modules/admin/widgets/ModuleControls.php new file mode 100644 index 0000000000..89edad73af --- /dev/null +++ b/protected/humhub/modules/admin/widgets/ModuleControls.php @@ -0,0 +1,164 @@ +initControls(); + return parent::init(); + } + + public function initControls() + { + $this->initInstalledModuleControls(); + + if ($marketplaceUrl = $this->getMarketplaceUrl($this->module)) { + $this->addEntry(new MenuLink([ + 'id' => 'marketplace-info', + 'label' => Yii::t('AdminModule.base', 'Information'), + 'url' => $marketplaceUrl, + 'htmlOptions' => ['rel' => 'noopener', 'target' => '_blank'], + 'icon' => 'external-link', + 'sortOrder' => 500, + ])); + } else { + $this->addEntry(new MenuLink([ + 'id' => 'info', + 'label' => Yii::t('AdminModule.base', 'Information'), + 'url' => ['/admin/module/info', 'moduleId' => $this->module->id], + 'htmlOptions' => ['data-target' => '#globalModal'], + 'icon' => 'info-circle', + 'sortOrder' => 600, + ])); + } + } + + private function initInstalledModuleControls() + { + if (!($this->module instanceof Module)) { + return; + } + + if ($this->module->isActivated) { + if ($this->module->getConfigUrl() != '') { + $this->addEntry(new MenuLink([ + 'id' => 'configure', + 'label' => Yii::t('AdminModule.base', 'Configure'), + 'url' => $this->module->getConfigUrl(), + 'icon' => 'wrench', + 'sortOrder' => 100, + ])); + } + + if ($this->module instanceof ContentContainerModule) { + $this->addEntry(new MenuLink([ + 'id' => 'default', + 'label' => Yii::t('AdminModule.base', 'Set as default'), + 'url' => ['/admin/module/set-as-default', 'moduleId' => $this->module->id], + 'htmlOptions' => ['data-target' => '#globalModal'], + 'icon' => 'check-square', + 'sortOrder' => 200, + ])); + } + + $this->addEntry(new MenuLink([ + 'id' => 'deactivate', + 'label' => Yii::t('AdminModule.base', 'Deactivate'), + 'url' => ['/admin/module/disable', 'moduleId' => $this->module->id], + 'htmlOptions' => [ + 'data-method' => 'POST', + 'data-confirm' => Yii::t('AdminModule.modules', 'Are you sure? *ALL* module data will be lost!'), + ], + 'icon' => 'minus-circle', + 'sortOrder' => 300, + ])); + } else { + $this->addEntry(new MenuLink([ + 'id' => 'deactivate', + 'label' => Yii::t('AdminModule.base', 'Activate'), + 'url' => ['/admin/module/enable', 'moduleId' => $this->module->id], + 'htmlOptions' => [ + 'data-method' => 'POST', + 'data-loader' => 'modal', + 'data-message' => Yii::t('AdminModule.modules', 'Enable module...'), + ], + 'icon' => 'check-circle', + 'sortOrder' => 300, + ])); + } + + if (Yii::$app->moduleManager->canRemoveModule($this->module->id)) { + $this->addEntry(new MenuLink([ + 'id' => 'uninstall', + 'label' => Yii::t('AdminModule.base', 'Uninstall'), + 'url' => ['/admin/module/remove', 'moduleId' => $this->module->id], + 'htmlOptions' => [ + 'data-method' => 'POST', + 'data-confirm' => Yii::t('AdminModule.modules', 'Are you sure? *ALL* module related data and files will be lost!'), + ], + 'icon' => 'trash', + 'sortOrder' => 400, + ])); + } + } + + private function getMarketplaceUrl($module): ?string + { + if (!Yii::$app->hasModule('marketplace')) { + return false; + } + + static $onlineModules; + + if (!isset($modules)) { + /* @var \humhub\modules\marketplace\Module $marketplaceModule */ + $marketplaceModule = Yii::$app->getModule('marketplace'); + $onlineModules = $marketplaceModule->onlineModuleManager->getModules(); + } + + return empty($onlineModules[$module->id]['marketplaceUrl']) + ? null + : $onlineModules[$module->id]['marketplaceUrl']; + } + + /** + * @inheritdoc + */ + public function getAttributes() + { + return [ + 'class' => 'nav nav-pills preferences' + ]; + } + +} diff --git a/protected/humhub/modules/admin/widgets/ModuleFilters.php b/protected/humhub/modules/admin/widgets/ModuleFilters.php new file mode 100644 index 0000000000..d927c63504 --- /dev/null +++ b/protected/humhub/modules/admin/widgets/ModuleFilters.php @@ -0,0 +1,45 @@ +addFilter('keyword', [ + 'title' => Yii::t('AdminModule.base', 'Search'), + 'placeholder' => Yii::t('AdminModule.base', 'Search...'), + 'type' => 'input', + 'wrapperClass' => 'col-md-7 form-search-filter-keyword', + 'afterInput' => Html::submitButton(Icon::get('search'), ['class' => 'form-button-search']), + 'sortOrder' => 100, + ]); + } + +} diff --git a/protected/humhub/modules/admin/widgets/ModuleStatus.php b/protected/humhub/modules/admin/widgets/ModuleStatus.php new file mode 100644 index 0000000000..7dc8281134 --- /dev/null +++ b/protected/humhub/modules/admin/widgets/ModuleStatus.php @@ -0,0 +1,105 @@ +{status}
'; + + /** + * @var string|null Cached status of the module + */ + private $_status; + + /** + * @inheritdoc + */ + public function run() + { + return str_replace(['{status}', '{class}'], [$this->statusTitle, $this->class], $this->template); + } + + /** + * @return false|string|null + */ + public function getStatus() + { + if ($this->_status !== null) { + return $this->_status; + } + + $onlineModule = new OnlineModule(['module' => $this->module]); + + if ($onlineModule->isProOnly) { + $this->_status = 'professional'; + } else if ($onlineModule->isFeatured) { + $this->_status = 'featured'; + } else if (!$onlineModule->isThirdParty) { + $this->_status = 'official'; + } else if ($onlineModule->isPartner) { + $this->_status = 'partner'; + } else if ($onlineModule->isDeprecated) { + $this->_status = 'deprecated'; + } else { + $this->_status = 'none'; + } + // TODO: Implement new status detection + + return $this->_status; + } + + public function getStatusTitle(): string + { + switch ($this->status) { + case 'professional': + return Yii::t('AdminModule.modules', 'Professional Edition'); + case 'featured': + return Yii::t('AdminModule.modules', 'Featured'); + case 'official': + return Yii::t('AdminModule.modules', 'Official'); + case 'partner': + return Yii::t('AdminModule.modules', 'Partner'); + case 'deprecated': + return Yii::t('AdminModule.modules', 'Deprecated'); + case 'new': + return Yii::t('AdminModule.modules', 'New'); + } + + return ''; + } + + public function getClass(): string + { + return 'card-status-' . $this->status; + } + +} diff --git a/protected/humhub/modules/admin/widgets/Modules.php b/protected/humhub/modules/admin/widgets/Modules.php new file mode 100644 index 0000000000..b850964b6b --- /dev/null +++ b/protected/humhub/modules/admin/widgets/Modules.php @@ -0,0 +1,107 @@ +initDefaultGroups(); + + parent::init(); + + ArrayHelper::multisort($this->groups, 'sortOrder'); + } + + private function initDefaultGroups() + { + /* @var Module $marketplaceModule */ + $marketplaceModule = Yii::$app->getModule('marketplace'); + if ($marketplaceModule->isFilteredBySingleTag('not_installed')) { + return; + } + + $installedModules = Yii::$app->moduleManager->getModules(); + + ArrayHelper::multisort($installedModules, 'isActivated', SORT_DESC); + + $this->addGroup('installed', [ + 'title' => Yii::t('AdminModule.modules', 'Installed'), + 'modules' => Yii::$app->moduleManager->filterModules($installedModules), + 'count' => count($installedModules), + 'noModulesMessage' => Yii::t('AdminModule.base', 'No modules installed yet. Install some to enhance the functionality!'), + 'sortOrder' => 100, + ]); + } + + public function addGroup(string $groupType, array $group) + { + $this->groups[$groupType] = $group; + } + + /** + * @inheritdoc + */ + public function run() + { + $modules = ''; + + $alwaysVisibleGroup = 'availableUpdates'; + $displaySingleGroup = true; + foreach ($this->groups as $groupType => $group) { + if ($groupType !== $alwaysVisibleGroup && !empty($group['modules'])) { + $displaySingleGroup = false; + break; + } + } + + $singleGroupPrinted = false; + foreach ($this->groups as $groupType => $group) { + if ($singleGroupPrinted) { + continue; + } + if (empty($group['count'])) { + continue; + } + if ($displaySingleGroup && $groupType !== $alwaysVisibleGroup) { + $singleGroupPrinted = true; + $group['title'] = false; + } + $group['type'] = $groupType; + $renderedGroup = $this->render('moduleGroup', $group); + + if (isset($group['groupTemplate'])) { + $renderedGroup = str_replace('{group}', $renderedGroup, $group['groupTemplate']); + } + + $modules .= $renderedGroup; + } + + return $modules; + } + +} diff --git a/protected/humhub/modules/admin/widgets/views/moduleCard.php b/protected/humhub/modules/admin/widgets/views/moduleCard.php new file mode 100644 index 0000000000..7dc4753091 --- /dev/null +++ b/protected/humhub/modules/admin/widgets/views/moduleCard.php @@ -0,0 +1,35 @@ + +
+ $module]) ?> +
+ getImage(), [ + 'class' => 'media-object img-rounded', + 'data-src' => 'holder.js/94x94', + 'alt' => '94x94', + 'style' => 'width:94px;height:94px', + ]) ?> + $module]) ?> +
+
+
getName() . ($isFeaturedModule ? ' ' . Icon::get('star')->color('info') : '') ?>
+
getVersion() ?>
+
getDescription() ?>
+
+ $module]) ?> +
\ No newline at end of file diff --git a/protected/humhub/modules/admin/widgets/views/moduleControls.php b/protected/humhub/modules/admin/widgets/views/moduleControls.php new file mode 100644 index 0000000000..f303e49daf --- /dev/null +++ b/protected/humhub/modules/admin/widgets/views/moduleControls.php @@ -0,0 +1,31 @@ + + + + + diff --git a/protected/humhub/modules/admin/widgets/views/moduleGroup.php b/protected/humhub/modules/admin/widgets/views/moduleGroup.php new file mode 100644 index 0000000000..f704305f17 --- /dev/null +++ b/protected/humhub/modules/admin/widgets/views/moduleGroup.php @@ -0,0 +1,42 @@ + + +

()

+ + +
+ +
+ +
+ + + + +
+ + + + $module, + 'view' => $view ?? null, + 'template' => $moduleTemplate ?? null, + ]); ?> + +
diff --git a/protected/humhub/modules/content/components/ContentContainerModuleManager.php b/protected/humhub/modules/content/components/ContentContainerModuleManager.php index ac58c27ab2..cfabd00a9d 100644 --- a/protected/humhub/modules/content/components/ContentContainerModuleManager.php +++ b/protected/humhub/modules/content/components/ContentContainerModuleManager.php @@ -156,7 +156,7 @@ class ContentContainerModuleManager extends \yii\base\Component $this->_available = []; foreach (Yii::$app->moduleManager->getModules() as $id => $module) { - if ($module instanceof ContentContainerModule && Yii::$app->hasModule($module->id) && + if ($module instanceof ContentContainerModule && $module->isActivated && $module->hasContentContainerType($this->contentContainer->className())) { $this->_available[$module->id] = $module; } diff --git a/protected/humhub/modules/content/widgets/ModuleActionButtons.php b/protected/humhub/modules/content/widgets/ModuleActionButtons.php new file mode 100644 index 0000000000..5efd0ac4a5 --- /dev/null +++ b/protected/humhub/modules/content/widgets/ModuleActionButtons.php @@ -0,0 +1,108 @@ +{buttons}
'; + + /** + * @inheritdoc + */ + public function run() + { + $html = ''; + + if ($this->module->getContentContainerConfigUrl($this->contentContainer) && $this->contentContainer->isModuleEnabled($this->module->id)) { + $html .= Button::asLink(Yii::t('ContentModule.modules', 'Configure'), $this->module->getContentContainerConfigUrl($this->contentContainer)) + ->cssClass('btn btn-sm btn-info configure-module-' . $this->module->id); + } + + if ($this->contentContainer->canDisableModule($this->module->id)) { + $html .= Button::asLink('  ' . Yii::t('ContentModule.modules', 'Activated'), '#') + ->cssClass('btn btn-sm btn-info active disable disable-module-' . $this->module->id) + ->style($this->contentContainer->isModuleEnabled($this->module->id) ? '' : 'display:none') + ->options([ + 'data-action-click' => 'content.container.disableModule', + 'data-action-url' => $this->getDisableUrl(), + 'data-reload' => '1', + 'data-action-confirm' => $this->getDisableConfirmationText(), + 'data-ui-loader' => 1, + ]); + } + + $html .= Button::asLink(Yii::t('ContentModule.modules', 'Enable'), '#') + ->cssClass('btn btn-sm btn-info enable enable-module-' . $this->module->id) + ->style($this->contentContainer->isModuleEnabled($this->module->id) ? 'display:none' : '') + ->options([ + 'data-action-click' => 'content.container.enableModule', + 'data-action-url' => $this->getEnableUrl(), + 'data-reload' => '1', + 'data-ui-loader' => 1, + ]); + + if (trim($html) === '') { + return ''; + } + + return str_replace('{buttons}', $html, $this->template); + } + + private function isSpace(): bool + { + return $this->contentContainer instanceof Space; + } + + private function getDisableUrl(): string + { + $route = $this->isSpace() ? '/space/manage/module/disable' : '/user/account/disable-module'; + return $this->contentContainer->createUrl($route, ['moduleId' => $this->module->id]); + } + + private function getDisableConfirmationText(): string + { + return $this->isSpace() + ? Yii::t('ContentModule.manage', 'Are you sure? *ALL* module data for this space will be deleted!') + : Yii::t('ContentModule.manage', 'Are you really sure? *ALL* module data for your profile will be deleted!'); + } + + private function getEnableUrl(): string + { + $route = $this->isSpace() ? '/space/manage/module/enable' : '/user/account/enable-module'; + return $this->contentContainer->createUrl($route, ['moduleId' => $this->module->id]); + } + +} diff --git a/protected/humhub/modules/content/widgets/ModuleCard.php b/protected/humhub/modules/content/widgets/ModuleCard.php new file mode 100644 index 0000000000..394fb8835e --- /dev/null +++ b/protected/humhub/modules/content/widgets/ModuleCard.php @@ -0,0 +1,40 @@ +render($this->view, [ + 'module' => $this->module, + 'contentContainer' => $this->contentContainer, + ]); + + return str_replace('{card}', $card, $this->template); + } + +} diff --git a/protected/humhub/modules/content/widgets/views/moduleCard.php b/protected/humhub/modules/content/widgets/views/moduleCard.php new file mode 100644 index 0000000000..b6fc641e76 --- /dev/null +++ b/protected/humhub/modules/content/widgets/views/moduleCard.php @@ -0,0 +1,32 @@ + +
+
+ getImage(), [ + 'class' => 'media-object img-rounded', + 'data-src' => 'holder.js/94x94', + 'alt' => '94x94', + 'style' => 'width:94px;height:94px', + ]) ?> +
+
+
getName() ?>
+
+ $module, + 'contentContainer' => $contentContainer, + ]) ?> +
\ No newline at end of file diff --git a/protected/humhub/modules/file/handler/FileHandlerCollection.php b/protected/humhub/modules/file/handler/FileHandlerCollection.php index b4faa5d1b0..6a71ea0260 100644 --- a/protected/humhub/modules/file/handler/FileHandlerCollection.php +++ b/protected/humhub/modules/file/handler/FileHandlerCollection.php @@ -39,12 +39,12 @@ class FileHandlerCollection extends \yii\base\Component public $type; /** - * @var \humhub\modules\file\models\File + * @var \humhub\modules\file\models\File */ public $file = null; /** - * @var type + * @var BaseFileHandler[] */ public $handlers = []; @@ -76,7 +76,7 @@ class FileHandlerCollection extends \yii\base\Component /** * Returns registered handlers by type - * + * * @param string|array $type or multiple type array * @param \humhub\modules\file\models\File $file the file (optional) * @return BaseFileHandler[] the registered handlers diff --git a/protected/humhub/modules/marketplace/Events.php b/protected/humhub/modules/marketplace/Events.php index 47a0fb2fb1..8d804eabc8 100644 --- a/protected/humhub/modules/marketplace/Events.php +++ b/protected/humhub/modules/marketplace/Events.php @@ -8,10 +8,21 @@ namespace humhub\modules\marketplace; +use humhub\components\Module as CoreModule; +use humhub\components\OnlineModule; +use humhub\modules\admin\events\ModulesEvent; +use humhub\modules\admin\libs\HumHubAPI; +use humhub\modules\admin\widgets\ModuleControls; +use humhub\modules\admin\widgets\ModuleFilters; +use humhub\modules\admin\widgets\Modules; +use humhub\modules\marketplace\models\Module as ModelModule; +use humhub\modules\share_between_humhub\helpers\Url; +use humhub\modules\ui\icon\widgets\Icon; +use humhub\modules\ui\menu\MenuLink; +use humhub\widgets\Button; use Yii; use yii\base\BaseObject; use yii\base\Event; -use yii\helpers\Url; class Events extends BaseObject { @@ -23,10 +34,7 @@ class Events extends BaseObject */ public static function onConsoleApplicationInit($event) { - /** @var Module $module */ - $module = Yii::$app->getModule('marketplace'); - - if (!$module->enabled) { + if (!self::getEnabledMarketplaceModule()) { return; } @@ -34,50 +42,291 @@ class Events extends BaseObject $application->controllerMap['module'] = commands\MarketplaceController::class; } - public static function onAdminModuleMenuInit($events) - { - /** @var Module $module */ - $module = Yii::$app->getModule('marketplace'); - - if (!$module->enabled) { - return; - } - - $updatesBadge = ''; - $updatesCount = count($module->onlineModuleManager->getModuleUpdates()); - if ($updatesCount > 0) { - $updatesBadge = '  ' . $updatesCount . ''; - } else { - $updatesBadge = '  0'; - } - - $events->sender->addItem([ - 'label' => Yii::t('MarketplaceModule.base', 'Browse online'), - 'url' => Url::to(['/marketplace/browse']), - 'sortOrder' => 200, - 'isActive' => (Yii::$app->controller->id == 'browse'), - ]); - - $events->sender->addItem([ - 'label' => Yii::t('MarketplaceModule.base', 'Purchases'), - 'url' => Url::to(['/marketplace/purchase']), - 'sortOrder' => 300, - 'isActive' => (Yii::$app->controller->id == 'purchase'), - ]); - - $events->sender->addItem([ - 'label' => Yii::t('MarketplaceModule.base', 'Available updates') . $updatesBadge, - 'url' => Url::to(['/marketplace/update']), - 'sortOrder' => 400, - 'isActive' => (Yii::$app->controller->id == 'update'), - ]); - - } - public static function onHourlyCron($event) { Yii::$app->queue->push(new jobs\PeActiveCheckJob()); Yii::$app->queue->push(new jobs\ModuleCleanupsJob()); } + private static function getEnabledMarketplaceModule(): ?Module + { + /* @var Module $marketplaceModule */ + $marketplaceModule = Yii::$app->getModule('marketplace'); + + return $marketplaceModule->enabled ? $marketplaceModule : null; + } + + public static function onAdminModuleFiltersInit($event) + { + if (!($marketplaceModule = self::getEnabledMarketplaceModule())) { + return; + } + + /* @var ModuleFilters $moduleFilters */ + $moduleFilters = $event->sender; + + $marketplaceModule->onlineModuleManager->getModules(); + $categories = $marketplaceModule->onlineModuleManager->getCategories(); + if (!empty($categories)) { + $moduleFilters->addFilter('categoryId', [ + 'title' => Yii::t('MarketplaceModule.base', 'Categories'), + 'type' => 'dropdown', + 'options' => $categories, + 'wrapperClass' => 'col-md-3', + 'sortOrder' => 200, + ]); + } + + $moduleFilters->addFilter('tags', [ + 'title' => Yii::t('MarketplaceModule.base', 'Tags'), + 'type' => 'tags', + 'tags' => [ + '' => Yii::t('MarketplaceModule.base', 'All'), + 'installed' => Yii::t('MarketplaceModule.base', 'Installed'), + 'not_installed' => Yii::t('MarketplaceModule.base', 'Not Installed'), + 'professional' => Yii::t('MarketplaceModule.base', 'Professional Edition'), + 'featured' => Yii::t('MarketplaceModule.base', 'Featured'), + 'official' => Yii::t('MarketplaceModule.base', 'Official'), + 'partner' => Yii::t('MarketplaceModule.base', 'Partner'), + 'new' => Yii::t('MarketplaceModule.base', 'New'), + ], + 'wrapperClass' => 'col-md-12 form-search-filter-tags', + 'sortOrder' => 20000, + ]); + } + + public static function onAdminModuleFiltersAfterRun($event) + { + if (!self::getEnabledMarketplaceModule()) { + return; + } + + $latestVersion = HumHubAPI::getLatestHumHubVersion(); + if (!$latestVersion) { + return; + } + + if (version_compare($latestVersion, Yii::$app->version, '>')) { + $updateUrl = 'https://docs.humhub.org/docs/admin/updating/'; + if (Yii::$app->hasModule('updater')) { + $updateUrl = Url::to(['/updater/update']); + } + + $info = [ + 'class' => 'directory-filters-footer-warning', + 'icon' => 'info-circle', + 'info' => Yii::t('MarketplaceModule.base', 'A new HumHub update is available. Install it now to keep your network up to date and to have access to the latest module versions.'), + 'link' => Button::asLink(Yii::t('MarketplaceModule.base', 'Update HumHub now'), $updateUrl) + ->cssClass('btn btn-primary'), + ]; + } else { + $info = [ + 'class' => 'directory-filters-footer-info', + 'icon' => 'check-circle', + 'info' => Yii::t('MarketplaceModule.base', 'This HumHub installation is up to date!'), + 'link' => Button::asLink('https://www.humhub.com', 'https://www.humhub.com') + ->cssClass('btn btn-info'), + ]; + } + + /* @var ModuleFilters $moduleFilters */ + $moduleFilters = $event->sender; + $event->result .= $moduleFilters->render('@humhub/modules/marketplace/widgets/views/moduleUpdateInfo', $info); + } + + public static function onAdminModulesInit($event) + { + if (!($marketplaceModule = self::getEnabledMarketplaceModule())) { + return; + } + + /* @var Modules $modulesWidget */ + $modulesWidget = $event->sender; + + $updateModules = $marketplaceModule->onlineModuleManager->getAvailableUpdateModules(); + if ($updateModulesCount = count($updateModules)) { + $updateAllButton = Button::info(Yii::t('MarketplaceModule.base', 'Update all')) + ->options([ + 'data-stop-title' => Icon::get('pause') . '   ' . Yii::t('MarketplaceModule.base', 'Stop updating'), + 'data-stop-class' => 'btn btn-warning pull-right', + ]) + ->action('marketplace.updateAll') + ->loader(false) + ->cssClass('active pull-right'); + + $modulesWidget->addGroup('availableUpdates', [ + 'title' => Yii::t('MarketplaceModule.base', 'Available Updates'), + 'modules' => $updateModules, + 'count' => $updateModulesCount, + 'view' => '@humhub/modules/marketplace/widgets/views/moduleUpdateCard', + 'groupTemplate' => '
' . $updateAllButton . '{group}
', + 'moduleTemplate' => '
{card}
', + 'sortOrder' => 10, + ]); + } + + if (!$marketplaceModule->isFilteredBySingleTag('installed')) { + $onlineModules = $marketplaceModule->onlineModuleManager->getNotInstalledModules(); + if ($onlineModulesCount = count($onlineModules)) { + $modulesWidget->addGroup('notInstalled', [ + 'title' => Yii::t('AdminModule.modules', 'Not Installed'), + 'modules' => Yii::$app->moduleManager->filterModules($onlineModules), + 'count' => $onlineModulesCount, + 'view' => '@humhub/modules/marketplace/widgets/views/moduleInstallCard', + 'sortOrder' => 200, + ]); + } + } + } + + public static function onAdminModuleManagerAfterFilterModules(ModulesEvent $event) + { + if (!self::getEnabledMarketplaceModule()) { + return; + } + + if (!is_array($event->modules)) { + return; + } + + foreach ($event->modules as $m => $module) { + if (!self::isFilteredModule($module)) { + unset($event->modules[$m]); + } + } + } + + /** + * @param CoreModule|ModelModule $module + * @return bool + */ + private static function isFilteredModule($module): bool + { + return self::isFilteredModuleByCategory($module) && + self::isFilteredModuleByTags($module); + } + + /** + * @param CoreModule|ModelModule $module + * @return bool + */ + private static function isFilteredModuleByCategory($module): bool + { + $categoryId = Yii::$app->request->get('categoryId', null); + + if (empty($categoryId)) { + return true; + } + + $moduleCategories = (new OnlineModule(['module' => $module]))->categories; + + return empty($moduleCategories) ? false : in_array($categoryId, $moduleCategories); + } + + /** + * @param CoreModule|ModelModule $module + * @return bool + */ + private static function isFilteredModuleByTags($module): bool + { + $tags = Yii::$app->request->get('tags', null); + + if (empty($tags)) { + return true; + } + + $tags = explode(',', $tags); + + $onlineModule = new OnlineModule(['module' => $module]); + + $searchInstalled = in_array('installed', $tags); + $searchNotInstalled = in_array('not_installed', $tags); + if ($searchInstalled && $searchNotInstalled && count($tags) === 2) { + // No need to filter when only 2 tags "Installed" and "Not Installed" are selected + return true; + } + if ($searchInstalled && !$searchNotInstalled && !$onlineModule->isInstalled) { + // Exclude all NOT Installed modules when requested only Installed modules + return false; + } + if (!$searchInstalled && $searchNotInstalled && $onlineModule->isInstalled) { + // Exclude all Installed modules when requested only NOT Installed modules + return false; + } + if (($searchInstalled || $searchNotInstalled) && count($tags) === 1) { + // No need to next filter when only 1 tag "Installed" or "Not Installed" is selected + return true; + } + + foreach ($tags as $tag) { + switch ($tag) { + case 'professional': + if ($onlineModule->isProOnly) { + return true; + } + break; + case 'featured': + if ($onlineModule->isFeatured) { + return true; + } + break; + case 'official': + if (!$onlineModule->isThirdParty) { + return true; + } + break; + case 'partner': + if ($onlineModule->isPartner) { + return true; + } + break; + case 'new': + // TODO: Filter by new status + break; + } + } + + return false; + } + + public static function onAdminModuleControlsInit($event) + { + if (!self::getEnabledMarketplaceModule()) { + return; + } + + /* @var ModuleControls $moduleControls */ + $moduleControls = $event->sender; + + $module = $moduleControls->module; + + if (!($module instanceof ModelModule)) { + return; + } + + /** @var \humhub\modules\marketplace\models\Module $module */ + + if ($module->isNonFree) { + $moduleControls->addEntry(new MenuLink([ + 'id' => 'marketplace-licence-key', + 'label' => Yii::t('MarketplaceModule.base', 'Add Licence Key'), + 'url' => ['/marketplace/purchase'], + 'htmlOptions' => ['data-target' => '#globalModal'], + 'icon' => 'key', + 'sortOrder' => 1000, + ])); + } + + if ($module->isThirdParty) { + $moduleControls->addEntry(new MenuLink([ + 'id' => 'marketplace-third-party', + 'label' => Yii::t('MarketplaceModule.base', 'Third-party') + . ($module->isCommunity ? ' - ' . Yii::t('MarketplaceModule.base', 'Community') : ''), + 'url' => ['/marketplace/browse/thirdparty-disclaimer'], + 'htmlOptions' => ['data-target' => '#globalModal'], + 'icon' => 'info-circle', + 'sortOrder' => 1100, + ])); + } + } } diff --git a/protected/humhub/modules/marketplace/Module.php b/protected/humhub/modules/marketplace/Module.php index 4c113021c5..97f9693091 100644 --- a/protected/humhub/modules/marketplace/Module.php +++ b/protected/humhub/modules/marketplace/Module.php @@ -9,7 +9,6 @@ namespace humhub\modules\marketplace; use humhub\components\Module as BaseModule; -use humhub\models\Setting; use humhub\modules\marketplace\components\HumHubApiClient; use humhub\modules\marketplace\components\LicenceManager; use humhub\modules\marketplace\models\Licence; @@ -20,6 +19,7 @@ use Yii; * The Marketplace modules contains all the capabilities to interact with the offical HumHub marketplace. * The core functions are the ability to easily install or update modules from the remote module directory. * + * @property-read Licence $licence * @property OnlineModuleManager $onlineModuleManager * @since 1.4 */ @@ -127,4 +127,23 @@ class Module extends BaseModule return $this->_humhubApi; } + + /** + * Check if the modules list is filtered by single tag + * + * @param string $tag + * @return bool + */ + public function isFilteredBySingleTag(string $tag): bool + { + $tags = Yii::$app->request->get('tags', null); + + if (empty($tags)) { + return false; + } + + $tags = explode(',', $tags); + + return count($tags) === 1 && $tags[0] == $tag; + } } diff --git a/protected/humhub/modules/marketplace/assets/Assets.php b/protected/humhub/modules/marketplace/assets/Assets.php new file mode 100644 index 0000000000..d4b854b277 --- /dev/null +++ b/protected/humhub/modules/marketplace/assets/Assets.php @@ -0,0 +1,32 @@ +getModule('marketplace'); + + $modules = $this->getModules(); + + foreach ($modules as $o => $module) { + $onlineModule = new ModelModule($module); + if ($onlineModule->isInstalled() || + ($onlineModule->isDeprecated && $marketplaceModule->hideLegacyModules)) { + unset($modules[$o]); + continue; + } + $modules[$o] = $onlineModule; + } + + return $modules; + } + + /** + * Get modules with available update + * + * @return ModelModule[] + */ + public function getAvailableUpdateModules(): array + { + $modules = $this->getModuleUpdates(false); + + foreach ($modules as $o => $module) { + $modules[$o] = new ModelModule($module); + } + + return $modules; + } + } diff --git a/protected/humhub/modules/marketplace/config.php b/protected/humhub/modules/marketplace/config.php index 53082cde1f..42762901b5 100644 --- a/protected/humhub/modules/marketplace/config.php +++ b/protected/humhub/modules/marketplace/config.php @@ -7,8 +7,11 @@ */ use humhub\commands\CronController; +use humhub\components\ModuleManager; +use humhub\modules\admin\widgets\ModuleControls; +use humhub\modules\admin\widgets\ModuleFilters; +use humhub\modules\admin\widgets\Modules; use humhub\modules\marketplace\Events; -use humhub\modules\admin\widgets\ModuleMenu; use humhub\modules\marketplace\Module; /** @noinspection MissedFieldInspection */ @@ -21,7 +24,11 @@ return [ 'professional-edition' => 'humhub\modules\marketplace\commands\ProfessionalEditionController' ], 'events' => [ - [ModuleMenu::class, ModuleMenu::EVENT_INIT, [Events::class, 'onAdminModuleMenuInit']], [CronController::class, CronController::EVENT_ON_HOURLY_RUN, [Events::class, 'onHourlyCron']], + [ModuleFilters::class, ModuleFilters::EVENT_INIT, [Events::class, 'onAdminModuleFiltersInit']], + [ModuleFilters::class, ModuleFilters::EVENT_AFTER_RUN, [Events::class, 'onAdminModuleFiltersAfterRun']], + [Modules::class, Modules::EVENT_INIT, [Events::class, 'onAdminModulesInit']], + [ModuleManager::class, ModuleManager::EVENT_AFTER_FILTER_MODULES, [Events::class, 'onAdminModuleManagerAfterFilterModules']], + [ModuleControls::class, ModuleControls::EVENT_INIT, [Events::class, 'onAdminModuleControlsInit']], ] ]; diff --git a/protected/humhub/modules/marketplace/controllers/BrowseController.php b/protected/humhub/modules/marketplace/controllers/BrowseController.php index 984fc75e5b..c90841141b 100644 --- a/protected/humhub/modules/marketplace/controllers/BrowseController.php +++ b/protected/humhub/modules/marketplace/controllers/BrowseController.php @@ -8,6 +8,7 @@ namespace humhub\modules\marketplace\controllers; use humhub\modules\admin\components\Controller; +use humhub\modules\admin\permissions\ManageModules; use humhub\modules\marketplace\Module; use Yii; use yii\web\HttpException; @@ -20,74 +21,16 @@ use yii\web\HttpException; */ class BrowseController extends Controller { - /** - * @var string - */ - public $defaultAction = 'list'; - - /** - * @var string - */ - public $subLayout = '@admin/views/layouts/module'; - /** * @inheritdoc */ public function getAccessRules() { return [ - ['permissions' => \humhub\modules\admin\permissions\ManageModules::class] + ['permissions' => ManageModules::class] ]; } - /** - * Complete list of all modules - */ - public function actionList() - { - $keyword = Yii::$app->request->post('keyword', ""); - $categoryId = (int)Yii::$app->request->post('categoryId', 0); - $hideInstalled = (boolean)Yii::$app->request->post('hideInstalled'); - - // Include Community Modules Form Submit - if (!empty(Yii::$app->request->get('communitySwitch'))) { - $this->module->settings->set('includeCommunityModules', (empty(Yii::$app->request->post('includeCommunityModules'))) ? 0 : 1); - } - $includeCommunityModules = (boolean)$this->module->settings->get('includeCommunityModules'); - - $onlineModules = $this->module->onlineModuleManager; - $modules = $onlineModules->getModules(); - $categories = $onlineModules->getCategories(); - - foreach ($modules as $i => $module) { - if (!empty($categoryId) && !in_array($categoryId, $module['categories'])) { - unset($modules[$i]); - } - if (!empty($keyword) && stripos($module['name'], $keyword) === false && stripos($module['description'], $keyword) === false) { - unset($modules[$i]); - } - if ($hideInstalled && Yii::$app->moduleManager->hasModule($module['id'])) { - unset($modules[$i]); - } - if ($this->module->hideLegacyModules && !empty($module['isDeprecated'])) { - unset($modules[$i]); - } - if (!$includeCommunityModules && !empty($module['isCommunity'])) { - unset($modules[$i]); - } - } - - return $this->render('list', [ - 'modules' => $modules, - 'keyword' => $keyword, - 'categories' => $categories, - 'categoryId' => $categoryId, - 'hideInstalled' => $hideInstalled, - 'includeCommunityModules' => $includeCommunityModules, - 'licence' => $this->module->getLicence() - ]); - } - /** * Returns the thirdparty disclaimer diff --git a/protected/humhub/modules/marketplace/controllers/PurchaseController.php b/protected/humhub/modules/marketplace/controllers/PurchaseController.php index 12a8afe346..e255ed23d0 100644 --- a/protected/humhub/modules/marketplace/controllers/PurchaseController.php +++ b/protected/humhub/modules/marketplace/controllers/PurchaseController.php @@ -9,6 +9,7 @@ namespace humhub\modules\marketplace\controllers; use humhub\modules\admin\components\Controller; use humhub\modules\admin\libs\HumHubAPI; +use humhub\modules\admin\permissions\ManageModules; use humhub\modules\marketplace\Module; use Yii; @@ -26,18 +27,13 @@ class PurchaseController extends Controller */ public $defaultAction = 'list'; - /** - * @var string - */ - public $subLayout = '@admin/views/layouts/module'; - /** * @inheritdoc */ public function getAccessRules() { return [ - ['permissions' => \humhub\modules\admin\permissions\ManageModules::class] + ['permissions' => ManageModules::class] ]; } @@ -47,18 +43,18 @@ class PurchaseController extends Controller public function actionList() { $hasError = false; - $message = ""; + $message = ''; - $licenceKey = Yii::$app->request->post('licenceKey', ""); + $licenceKey = Yii::$app->request->post('licenceKey', ''); - if ($licenceKey != "") { + if ($licenceKey !== '') { $result = HumHubAPI::request('v1/modules/registerPaid', ['licenceKey' => $licenceKey]); if (!isset($result['status'])) { $hasError = true; $message = 'Could not connect to HumHub API!'; } elseif ($result['status'] == 'ok' || $result['status'] == 'created') { $message = 'Module licence added!'; - $licenceKey = ""; + $licenceKey = ''; } else { $hasError = true; $message = 'Invalid module licence key!'; @@ -75,46 +71,12 @@ class PurchaseController extends Controller } } - return $this->render('list', ['modules' => $modules, 'licenceKey' => $licenceKey, 'hasError' => $hasError, 'message' => $message]); - } - - - /** - * Complete list of all modules - */ - public function actionRegister() - { - $hasError = false; - $message = ""; - - $licenceKey = Yii::$app->request->post('licenceKey', ""); - if ($licenceKey != "") { - - $result = HumHubAPI::request('v1/modules/registerPaid', ['licenceKey' => $licenceKey]); - if (!isset($result['status'])) { - $hasError = true; - $message = 'Could not connect to HumHub API!'; - } elseif ($result['status'] == 'ok' || $result['status'] == 'created') { - $message = 'Module licence added!'; - $licenceKey = ""; - } else { - $hasError = true; - $message = 'Invalid module licence key!'; - } - - } - - // Only showed purchased modules - $onlineModules = $this->module->onlineModuleManager; - $modules = $onlineModules->getModules(false); - - foreach ($modules as $i => $module) { - if (!isset($module['purchased']) || !$module['purchased']) { - unset($modules[$i]); - } - } - - return $this->render('list', ['modules' => $modules, 'licenceKey' => $licenceKey, 'hasError' => $hasError, 'message' => $message]); + return $this->renderAjax('list', [ + 'modules' => $modules, + 'licenceKey' => $licenceKey, + 'hasError' => $hasError, + 'message' => $message, + ]); } } diff --git a/protected/humhub/modules/marketplace/controllers/UpdateController.php b/protected/humhub/modules/marketplace/controllers/UpdateController.php index 34ba4f11f4..f9079cca43 100644 --- a/protected/humhub/modules/marketplace/controllers/UpdateController.php +++ b/protected/humhub/modules/marketplace/controllers/UpdateController.php @@ -9,6 +9,7 @@ namespace humhub\modules\marketplace\controllers; use humhub\components\Module; use humhub\modules\admin\components\Controller; +use humhub\modules\admin\permissions\ManageModules; use Yii; use yii\web\HttpException; @@ -20,42 +21,16 @@ use yii\web\HttpException; */ class UpdateController extends Controller { - /** - * @var string - */ - public $defaultAction = 'list'; - - /** - * @var string - */ - public $subLayout = '@admin/views/layouts/module'; - /** * @inheritdoc */ public function getAccessRules() { return [ - ['permissions' => \humhub\modules\admin\permissions\ManageModules::class] + ['permissions' => ManageModules::class] ]; } - /** - * Lists all available module updates - */ - public function actionList() - { - // Include Community Modules Form Submit - if (!empty(Yii::$app->request->get('betaSwitch'))) { - $this->module->settings->set('includeBetaUpdates', (empty(Yii::$app->request->post('includeBetaUpdates'))) ? 0 : 1); - } - $includeBetaUpdates = (boolean)$this->module->settings->get('includeBetaUpdates'); - - $modules = $this->module->onlineModuleManager->getModuleUpdates(false); - return $this->render('list', ['modules' => $modules, 'includeBetaUpdates' => $includeBetaUpdates]); - } - - /** * Updates a module with the most recent version online * @@ -82,23 +57,30 @@ class UpdateController extends Controller if (empty($moduleInfo['latestCompatibleVersion']['downloadUrl'])) { if (!empty($moduleInfo['isPaid'])) { - $this->view->setStatusMessage('error', Yii::t('AdminModule.modules', 'License not found or expired. Please contact the module publisher.')); + $error = Yii::t('AdminModule.modules', 'License not found or expired. Please contact the module publisher.'); } else { - Yii::error('Could not determine module download url from HumHub API response.', 'marketplace'); + $error = 'Could not determine module download url from HumHub API response.'; + Yii::error($error, 'marketplace'); } - return $this->redirect(['/marketplace/update/list']); + throw new HttpException(500, $error); } $this->module->onlineModuleManager->update($moduleId); - try { $module->publishAssets(true); } catch (\Exception $e) { Yii::error($e); } - return $this->redirect(['/marketplace/update/list']); + return $this->asJson([ + 'success' => true, + 'status' => Yii::t('AdminModule.modules', 'Update successful'), + 'message' => Yii::t('AdminModule.modules', 'Module "{moduleName}" has been updated to version {newVersion} successfully.', [ + 'moduleName' => $moduleInfo['latestCompatibleVersion']['name'], + 'newVersion' => $moduleInfo['latestCompatibleVersion']['version'], + ]), + ]); } } diff --git a/protected/humhub/modules/marketplace/models/Module.php b/protected/humhub/modules/marketplace/models/Module.php new file mode 100644 index 0000000000..814d4de8c3 --- /dev/null +++ b/protected/humhub/modules/marketplace/models/Module.php @@ -0,0 +1,184 @@ + $value) { + if (!property_exists($this, $name)) { + // Exclude new unknown property from marketplace API to avoid error + unset($config[$name]); + } + } + + parent::__construct($config); + } + + public function getIsNonFree(): bool + { + return (!empty($this->price_eur) || !empty($this->price_request_quote)); + } + + public function getVersion(): string + { + return $this->latestVersion; + } + + public function getImage(): string + { + return empty($this->moduleImageUrl) + ? Yii::getAlias('@web-static/img/default_module.jpg') + : $this->moduleImageUrl; + } + + public function isInstalled(): bool + { + return Yii::$app->moduleManager->hasModule($this->id); + } + + public function isProFeature(): bool + { + return !empty($this->professional_only); + } + + public function isProOnly(): bool + { + if (!$this->isProFeature()) { + return false; + } + + /* @var MarketplaceModule */ + $marketplaceModule = Yii::$app->getModule('marketplace'); + $licence = $marketplaceModule->getLicence(); + + return $licence->type !== Licence::LICENCE_TYPE_PRO; + } + + public function getCheckoutUrl(): string + { + return str_replace('-returnToUrl-', Url::to(['/marketplace/purchase/list'], true), $this->checkoutUrl); + } +} diff --git a/protected/humhub/modules/marketplace/resources/css/modules.css b/protected/humhub/modules/marketplace/resources/css/modules.css new file mode 100644 index 0000000000..10060087a1 --- /dev/null +++ b/protected/humhub/modules/marketplace/resources/css/modules.css @@ -0,0 +1 @@ +@media (min-width:500px){.container-modules.container-fluid .container-module-updates .card{width:33.33333333%}}@media (min-width:1000px){.container-modules.container-fluid .container-module-updates .card{width:25%}}@media (min-width:1300px){.container-modules.container-fluid .container-module-updates .card{width:20%}}@media (min-width:1600px){.container-modules.container-fluid .container-module-updates .card{width:16.66666667%}}@media (min-width:1900px){.container-modules.container-fluid .container-module-updates .card{width:12.5%}}.container-module-updates{background:#02A1B1;margin-top:30px;padding:16px 10px 2px;border-radius:4px}.container-module-updates .row.cards{margin-right:-1px;margin-top:0}.container-module-updates .modules-type{color:#FFFFFF;margin:10px 0 30px}.container-module-updates .card{padding-right:1px}.container-module-updates .card .card-panel{color:#FFF;margin-top:0}.container-module-updates .card .card-panel>div:not(.card-status){background:#02717c}.container-module-updates .card .card-panel .card-header{padding:12px}.container-module-updates .card .card-panel .card-body{padding:4px 12px 20px;color:#FFF}.container-module-updates .card .card-panel .card-body .card-title{color:#FFF;font-size:14px}.container-module-updates .card .card-panel .card-footer{padding:0 12px 12px}.container-module-updates .card .card-panel .card-footer .btn-info{border-radius:4px}.container-module-updates .card .card-panel .card-footer .btn-info.active{border-color:#FFF}.container-module-updates .card .card-panel .card-footer .btn-info:not(.active){padding:0 4px;border:1px solid #FFF;background:#02717c;color:#FFF}.container-module-updates .card .card-panel .card-footer .btn-info:not(.active):hover,.container-module-updates .card .card-panel .card-footer .btn-info:not(.active):active{background:#047c88 !important;color:#FFF !important}.container-module-updates .card .card-panel .card-footer .btn-info[data-update-status=failed]{border-color:#fc314f}/*# sourceMappingURL=modules.css.map */ \ No newline at end of file diff --git a/protected/humhub/modules/marketplace/resources/css/modules.css.map b/protected/humhub/modules/marketplace/resources/css/modules.css.map new file mode 100644 index 0000000000..44456f50d0 --- /dev/null +++ b/protected/humhub/modules/marketplace/resources/css/modules.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["modules.less"],"names":[],"mappings":"AAEQ,QAA0B,iBAA1B,kBAFU,gBACd,0BAEQ,OACI,oBAGR,QAA2B,kBAA3B,kBAPU,gBACd,0BAOQ,OACI,WAGR,QAA2B,kBAA3B,kBAZU,gBACd,0BAYQ,OACI,WAGR,QAA2B,kBAA3B,kBAjBU,gBACd,0BAiBQ,OACI,oBAGR,QAA2B,kBAA3B,kBAtBU,gBACd,0BAsBQ,OACI,aAQhB,0BACI,kBAAA,CACA,eAAA,CACA,qBAAA,CACA,kBAJJ,yBAKI,KAAI,OACA,iBAAA,CACA,aAPR,yBASI,eACI,aAAA,CACA,mBAXR,yBAaI,OACI,kBAdR,yBAaI,MAEI,aACI,UAAA,CACA,aAjBZ,yBAaI,MAEI,YAGI,IAAK,IAAI,eACL,mBAnBhB,yBAaI,MAEI,YAMI,cACI,aAtBhB,yBAaI,MAEI,YASI,YACI,qBAAA,CACA,WA1BhB,yBAaI,MAEI,YASI,WAGI,aACI,UAAA,CACA,eA7BpB,yBAaI,MAEI,YAiBI,cACI,oBAjChB,yBAaI,MAEI,YAiBI,aAEI,WACI,kBACA,yBAvBhB,MAEI,YAiBI,aAEI,UAEK,QACG,kBAEJ,yBA1BhB,MAEI,YAiBI,aAEI,UAKK,IAAI,UACD,aAAA,CACA,qBAAA,CACA,kBAAA,CACA,WACA,yBA/BpB,MAEI,YAiBI,aAEI,UAKK,IAAI,SAKA,OAAQ,yBA/B7B,MAEI,YAiBI,aAEI,UAKK,IAAI,SAKS,QACN,kBAAA,YACA,UAAA,YAGR,yBApChB,MAEI,YAiBI,aAEI,UAeK,4BACG","file":"modules.css"} \ No newline at end of file diff --git a/protected/humhub/modules/marketplace/resources/css/modules.less b/protected/humhub/modules/marketplace/resources/css/modules.less new file mode 100644 index 0000000000..937078e0e8 --- /dev/null +++ b/protected/humhub/modules/marketplace/resources/css/modules.less @@ -0,0 +1,89 @@ +.container-modules.container-fluid { + .container-module-updates { + @media (min-width: 500px) { + .card { + width: 33.33333333%; + } + } + @media (min-width: 1000px) { + .card { + width: 25%; + } + } + @media (min-width: 1300px) { + .card { + width: 20%; + } + } + @media (min-width: 1600px) { + .card { + width: 16.66666667%; + } + } + @media (min-width: 1900px) { + .card { + width: 12.5%; + } + } + } +} + +@moduleUpdateLayoutBg: #02A1B1; +@moduleUpdateCardBg: #02717c; +.container-module-updates { + background: @moduleUpdateLayoutBg; + margin-top: 30px; + padding: 16px 10px 2px; + border-radius: 4px; + .row.cards { + margin-right: -1px; + margin-top: 0; + } + .modules-type { + color: #FFFFFF; + margin: 10px 0 30px; + } + .card { + padding-right: 1px; + .card-panel { + color: #FFF; + margin-top: 0; + > div:not(.card-status) { + background: @moduleUpdateCardBg; + } + .card-header { + padding: 12px; + } + .card-body { + padding: 4px 12px 20px; + color: #FFF; + .card-title { + color: #FFF; + font-size: 14px; + } + } + .card-footer { + padding: 0 12px 12px; + .btn-info { + border-radius: 4px; + &.active { + border-color: #FFF; + } + &:not(.active) { + padding: 0 4px; + border: 1px solid #FFF; + background: @moduleUpdateCardBg; + color: #FFF; + &:hover, &:active { + background: #047c88 !important; + color: #FFF !important; + } + } + &[data-update-status=failed] { + border-color: #fc314f; + } + } + } + } + } +} \ No newline at end of file diff --git a/protected/humhub/modules/marketplace/resources/js/humhub.marketplace.js b/protected/humhub/modules/marketplace/resources/js/humhub.marketplace.js new file mode 100644 index 0000000000..88c550e335 --- /dev/null +++ b/protected/humhub/modules/marketplace/resources/js/humhub.marketplace.js @@ -0,0 +1,134 @@ +humhub.module('marketplace', function (module, require, $) { + const client = require('client'); + const loader = require('ui.loader'); + const status = require('ui.status'); + + const update = function (evt) { + startUpdate(evt); + + client.post(evt).then(function (response) { + endSuccessUpdate(evt, response); + }).catch(function (err) { + endFailedUpdate(evt, err); + }); + }; + + const startUpdate = function(evt) { + const card = evt.$trigger.closest('.card'); + card.css({width: card.outerWidth(), height: card.outerHeight()}); + evt.$trigger.parent().hide(); + evt.$trigger.removeAttr('data-update-status'); + loader.set(evt.$trigger.parent().prev(), {size: '12px'}); + } + + const endSuccessUpdate = function (evt, response) { + const card = evt.$trigger.closest('.card'); + const body = card.find('.card-body'); + + body.html('
'); + const resultIcon = body.find('.fa').css({fontSize: 0, opacity: 0}); + resultIcon.animate({fontSize: '50px', opacity: 1}, 1000, function () { + $(this).after('
' + response.status + '
'); + setTimeout(function () { + card.css({ + position: 'absolute', + top: card.position().top, + left: card.position().left, + }); + + card.after('
'); + card.next() + .css({opacity: 0, minHeight: card.outerHeight()}) + .animate({width: 0}, 'slow', function () {$(this).remove()}); + card.animate({opacity: 0}, 'slow', function () { + $(this).remove(); + const availableUpdates = $('[data-action-click="marketplace.update"]').length; + $('.group-modules-count-availableUpdates').html(availableUpdates); + if (availableUpdates === 0) { + $('[data-action-click="marketplace.updateAll"]').remove(); + $('.container-module-updates').animate({opacity: 0, height: 0, padding: 0, margin: 0}, 2000); + } + }); + + module.log.success('success.saved', true); + status.success(response.message); + evt.$trigger.attr('data-update-status', 'success'); + + runNextUpdate(); + }, 1000); + }); + } + + const endFailedUpdate = function(evt, response) { + module.log.error(response); + status.error(response.message); + evt.$trigger.attr('data-update-status', 'failed') + .attr('title', response.message); + + evt.$trigger.parent().show(); + loader.reset(evt.$trigger.parent().prev()); + + runNextUpdate(); + } + + const updateAll = function (evt) { + const btn = evt.$trigger; + + if (btn.data('is-updating-all')) { + stopUpdateAll(); + return; + } + + btn.data('is-updating-all', true) + .data('orig-title', btn.html()) + .data('orig-class', btn.attr('class')) + .html(btn.data('stop-title')) + .attr('class', btn.data('stop-class')); + + $('[data-action-click="marketplace.update"]').removeAttr('data-update-status'); + + runNextUpdate(); + } + + const runNextUpdate = function() { + const updateAllButton = $('[data-action-click="marketplace.updateAll"]'); + if (!updateAllButton.data('is-updating-all')) { + return; + } + + const nextButton = $('[data-action-click="marketplace.update"]:not([data-update-status=failed]):first'); + if (nextButton.length) { + nextButton.click(); + } else { + stopUpdateAll(); + } + } + + const stopUpdateAll = function () { + const updateAllButton = $('[data-action-click="marketplace.updateAll"]'); + updateAllButton.data('is-updating-all', false) + .html(updateAllButton.data('orig-title')) + .attr('class', updateAllButton.data('orig-class')); + } + + const registerLicenceKey = function(evt) { + const form = evt.$trigger.closest('form'); + const licenceKey = form.find('input[name=licenceKey]').val(); + + loader.set(form); + + client.post(form.attr('action'), {data: {licenceKey}}).then(function (response) { + form.closest('.modal-dialog').after(response.html).remove(); + }).catch(function (err) { + module.log.error(err); + status.error(err.message); + loader.reset(form); + }); + } + + module.export({ + update, + updateAll, + registerLicenceKey, + }); +}); \ No newline at end of file diff --git a/protected/humhub/modules/marketplace/views/browse/_module.php b/protected/humhub/modules/marketplace/views/browse/_module.php deleted file mode 100644 index a20206505c..0000000000 --- a/protected/humhub/modules/marketplace/views/browse/_module.php +++ /dev/null @@ -1,105 +0,0 @@ -moduleManager->hasModule($module['id'])); - -$moduleImageUrl = Yii::getAlias('@web-static/img/default_module.jpg'); -if (isset($module['moduleImageUrl']) && $module['moduleImageUrl'] != "") { - $moduleImageUrl = $module['moduleImageUrl']; -} - - -$isProFeature = (!empty($module['professional_only'])); -$isProOnly = ($isProFeature && $licence->type !== Licence::LICENCE_TYPE_PRO); - -?> - -
-
- 64x64 - -
- -

- - - - -   - - - - - - - Professional Edition Feature - - - -

- -

- -
- - - - · - - - - -   Purchased - - - - - - · - - - - - · Professional Edition', ['/admin/information'], ['style' => 'font-weight:bold']); ?> - - - · 'font-weight:bold', 'target' => '_blank']); ?> - - - · $module['price_eur'] . '€']), $checkoutUrl, ['style' => 'font-weight:bold', 'target' => '_blank']); ?> - - · $module['id']]), ['style' => 'font-weight:bold', 'data-loader' => "modal", 'data-message' => Yii::t('MarketplaceModule.base', 'Installing module...'), 'data-method' => 'POST']); ?> - - - - - - · - - - · ' - , $module['marketplaceUrl'], ['target' => '_blank']); ?> - - - · '#globalModal']); ?> - - - - Community - - - -
-
-
diff --git a/protected/humhub/modules/marketplace/views/browse/list.php b/protected/humhub/modules/marketplace/views/browse/list.php deleted file mode 100644 index 54d7b5ea86..0000000000 --- a/protected/humhub/modules/marketplace/views/browse/list.php +++ /dev/null @@ -1,62 +0,0 @@ - -
- - 'form-search', 'id' => 'filterForm']); ?> -
-
- 'form-control', 'data-ui-select2' => '', 'id' => 'categorySelect']); ?> -
-
- -
-
- - - 1]), 'post', ['id' => 'communityForm']); ?> -
- -
- -
- - - -
- -

-
- - - - render('_module', ['module' => $module, 'licence' => $licence]); ?> - - -
- - - - diff --git a/protected/humhub/modules/marketplace/views/purchase/list.php b/protected/humhub/modules/marketplace/views/purchase/list.php index 9b8e9254cc..48ed4ebb4f 100644 --- a/protected/humhub/modules/marketplace/views/purchase/list.php +++ b/protected/humhub/modules/marketplace/views/purchase/list.php @@ -1,76 +1,92 @@ - -
- - 'form-search']); ?> -
-
-
- - -
- +'; + + /** + * @inheritdoc + */ + public function run() + { + $html = ''; + + if (!isset($this->module->latestCompatibleVersion) || $this->module->isInstalled()) { + return ''; + } + + if ($this->module->isProOnly()) { + $html .= Button::primary(Icon::get('info-circle') . '  ' . Yii::t('MarketplaceModule.base', 'Professional Edition')) + ->link(['/admin/information']) + ->sm() + ->loader(false) + ->tooltip(Yii::t('MarketplaceModule.base', 'This is a Professional Edition module. You do not have the permission to install this module. If you are interested in our product, you can find more information about our Professional Edition by clicking on this button.')); + } elseif (!empty($this->module->price_request_quote) && !$this->module->purchased) { + $html .= Button::primary(Yii::t('MarketplaceModule.base', 'Buy')) + ->link($this->module->checkoutUrl) + ->sm() + ->options(['target' => '_blank']) + ->loader(false); + } elseif (!empty($this->module->price_eur) && !$this->module->purchased) { + $html .= Button::primary(Yii::t('MarketplaceModule.base', 'Buy (%price%)', ['%price%' => $this->module->price_eur . '€'])) + ->link($this->module->checkoutUrl) + ->sm() + ->options(['target' => '_blank']) + ->loader(false); + } else { + $html .= Button::primary(Yii::t('MarketplaceModule.base', 'Install')) + ->link(['/marketplace/browse/install', 'moduleId' => $this->module->id]) + ->sm() + ->options([ + 'data-method' => 'POST', + 'data-loader' => 'modal', + 'data-message' => Yii::t('MarketplaceModule.base', 'Installing module...'), + ]); + } + + if (trim($html) === '') { + return ''; + } + + return str_replace('{buttons}', $html, $this->template); + } + +} diff --git a/protected/humhub/modules/marketplace/widgets/ModuleUpdateActionButtons.php b/protected/humhub/modules/marketplace/widgets/ModuleUpdateActionButtons.php new file mode 100644 index 0000000000..c4046d6368 --- /dev/null +++ b/protected/humhub/modules/marketplace/widgets/ModuleUpdateActionButtons.php @@ -0,0 +1,56 @@ +{buttons}
'; + + /** + * @inheritdoc + */ + public function run() + { + $html = ''; + + if (!isset($this->module->latestCompatibleVersion) || !$this->module->isInstalled()) { + return $html; + } + + $html .= Button::asLink(Yii::t('MarketplaceModule.base', 'Update'), ['/marketplace/update/install', 'moduleId' => $this->module->id]) + ->cssClass('btn btn-xs btn-info active') + ->options(['data-action-click' => 'marketplace.update']); + + $html .= Button::asLink(Yii::t('MarketplaceModule.base', 'Changelog'), $this->module->marketplaceUrl . '#changelog') + ->cssClass('btn btn-xs btn-info') + ->options(['target' => '_blank']); + + return str_replace('{buttons}', $html, $this->template); + } + +} diff --git a/protected/humhub/modules/marketplace/widgets/views/moduleInstallCard.php b/protected/humhub/modules/marketplace/widgets/views/moduleInstallCard.php new file mode 100644 index 0000000000..e3fe2c9de3 --- /dev/null +++ b/protected/humhub/modules/marketplace/widgets/views/moduleInstallCard.php @@ -0,0 +1,34 @@ + +
+ $module]) ?> +
+ image, [ + 'class' => 'media-object img-rounded', + 'data-src' => 'holder.js/94x94', + 'alt' => '94x94', + 'style' => 'width:94px;height:94px', + ]) ?> + $module]) ?> +
+
+
name . ($module->featured ? ' ' . Icon::get('star')->color('info') : '') ?>
+
latestVersion ?>
+
description ?>
+
+ $module]) ?> +
\ No newline at end of file diff --git a/protected/humhub/modules/marketplace/widgets/views/moduleUpdateCard.php b/protected/humhub/modules/marketplace/widgets/views/moduleUpdateCard.php new file mode 100644 index 0000000000..cf9b1a7e1a --- /dev/null +++ b/protected/humhub/modules/marketplace/widgets/views/moduleUpdateCard.php @@ -0,0 +1,33 @@ + +
+
+ image, [ + 'class' => 'media-object img-rounded', + 'data-src' => 'holder.js/60x60', + 'alt' => '60x60', + 'style' => 'width:60px;height:60px', + ]) ?> +
+
+
name ?>
+
moduleManager->getModule($module->id)->getVersion() ?> → latestCompatibleVersion ?>
+
+ $module]) ?> +
\ No newline at end of file diff --git a/protected/humhub/modules/marketplace/widgets/views/moduleUpdateInfo.php b/protected/humhub/modules/marketplace/widgets/views/moduleUpdateInfo.php new file mode 100644 index 0000000000..b8f387df84 --- /dev/null +++ b/protected/humhub/modules/marketplace/widgets/views/moduleUpdateInfo.php @@ -0,0 +1,23 @@ + + \ No newline at end of file diff --git a/protected/humhub/modules/space/controllers/SpacesController.php b/protected/humhub/modules/space/controllers/SpacesController.php index 5ea5cccb44..81bf3961dd 100644 --- a/protected/humhub/modules/space/controllers/SpacesController.php +++ b/protected/humhub/modules/space/controllers/SpacesController.php @@ -61,7 +61,7 @@ class SpacesController extends Controller $urlParams = Yii::$app->request->getQueryParams(); unset($urlParams['page']); array_unshift($urlParams, '/space/spaces/load-more'); - $this->getView()->registerJsConfig('directory', [ + $this->getView()->registerJsConfig('cards', [ 'loadMoreUrl' => Url::to($urlParams), ]); diff --git a/protected/humhub/modules/space/modules/manage/controllers/ModuleController.php b/protected/humhub/modules/space/modules/manage/controllers/ModuleController.php index 74850b3bf8..610b06debd 100644 --- a/protected/humhub/modules/space/modules/manage/controllers/ModuleController.php +++ b/protected/humhub/modules/space/modules/manage/controllers/ModuleController.php @@ -25,7 +25,10 @@ class ModuleController extends Controller { $space = $this->getSpace(); - return $this->render('index', ['availableModules' => $space->getAvailableModules(), 'space' => $space]); + return $this->render('index', [ + 'space' => $space, + 'modules' => $space->getAvailableModules(), + ]); } /** diff --git a/protected/humhub/modules/space/modules/manage/views/module/index.php b/protected/humhub/modules/space/modules/manage/views/module/index.php index cb2839ed14..9e81ea0d8b 100644 --- a/protected/humhub/modules/space/modules/manage/views/module/index.php +++ b/protected/humhub/modules/space/modules/manage/views/module/index.php @@ -1,61 +1,35 @@ +
+

Space Modules'); ?>

-
-
- Space Modules'); ?> -
-
- - -

- -
- - - - $module): ?> -
-
- 64x64 - -
-

getContentContainerName($space); ?> - isModuleEnabled($moduleId)) : ?> - - -

- -

getContentContainerDescription($space); ?>

- - canDisableModule($moduleId)) : ?> - - - - - - getContentContainerConfigUrl($space) && $space->isModuleEnabled($moduleId)) : ?> - - - - - - - - +
+ +
+
+
+ +
- + + + $space, + 'module' => $module, + ]); ?> +
-
+
\ No newline at end of file diff --git a/protected/humhub/modules/space/views/create/moduleCard.php b/protected/humhub/modules/space/views/create/moduleCard.php new file mode 100644 index 0000000000..55491038f1 --- /dev/null +++ b/protected/humhub/modules/space/views/create/moduleCard.php @@ -0,0 +1,31 @@ + +
+
+ getImage(), [ + 'class' => 'media-object img-rounded', + 'data-src' => 'holder.js/94x94', + 'alt' => '94x94', + 'style' => 'width:94px;height:94px', + ]) ?> +
+
+
getName() ?>
+
getContentContainerDescription($contentContainer), 75); ?>
+
+ $module, 'space' => $contentContainer]) ?> +
\ No newline at end of file diff --git a/protected/humhub/modules/space/views/create/modules.php b/protected/humhub/modules/space/views/create/modules.php index 3cbb8d223c..6eed3ec0af 100644 --- a/protected/humhub/modules/space/views/create/modules.php +++ b/protected/humhub/modules/space/views/create/modules.php @@ -1,15 +1,20 @@ -