Rework Directory (#5057)

Co-authored-by: Lucas Bartholemy <lucas@bartholemy.com>
This commit is contained in:
Yuriy Bakhtin 2021-06-05 14:38:43 +03:00 committed by GitHub
parent 95be120d81
commit afc8e51f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 2974 additions and 178 deletions

157
composer.lock generated
View File

@ -53,7 +53,7 @@
"version": "3.5.1",
"source": {
"type": "git",
"url": "https://github.com/jquery/jquery-dist.git",
"url": "git@github.com:jquery/jquery-dist.git",
"reference": "4c0e4becb8263bb5b3e6dadc448d8e7305ef8215"
},
"dist": {
@ -561,16 +561,16 @@
},
{
"name": "firebase/php-jwt",
"version": "v5.2.1",
"version": "v5.3.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "f42c9110abe98dd6cfe9053c49bc86acc70b2d23"
"reference": "3c2d70f2e64e2922345e89f2ceae47d2463faae1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/f42c9110abe98dd6cfe9053c49bc86acc70b2d23",
"reference": "f42c9110abe98dd6cfe9053c49bc86acc70b2d23",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/3c2d70f2e64e2922345e89f2ceae47d2463faae1",
"reference": "3c2d70f2e64e2922345e89f2ceae47d2463faae1",
"shasum": ""
},
"require": {
@ -609,9 +609,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v5.2.1"
"source": "https://github.com/firebase/php-jwt/tree/v5.3.0"
},
"time": "2021-02-12T00:02:00+00:00"
"time": "2021-05-20T17:37:02+00:00"
},
{
"name": "imagine/imagine",
@ -2671,16 +2671,16 @@
},
{
"name": "markbaker/complex",
"version": "2.0.2",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "d18272926d58065140314c01e18ec3dd7ae854ea"
"reference": "6f724d7e04606fd8adaa4e3bb381c3e9db09c946"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/d18272926d58065140314c01e18ec3dd7ae854ea",
"reference": "d18272926d58065140314c01e18ec3dd7ae854ea",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/6f724d7e04606fd8adaa4e3bb381c3e9db09c946",
"reference": "6f724d7e04606fd8adaa4e3bb381c3e9db09c946",
"shasum": ""
},
"require": {
@ -2760,9 +2760,9 @@
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/2.0.2"
"source": "https://github.com/MarkBaker/PHPComplex/tree/2.0.3"
},
"time": "2021-05-24T10:53:30+00:00"
"time": "2021-06-02T09:44:11+00:00"
},
{
"name": "markbaker/matrix",
@ -4013,16 +4013,16 @@
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.17.1",
"version": "1.18.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "c55269cb06911575a126dc225a05c0e4626e5fb4"
"reference": "418cd304e8e6b417ea79c3b29126a25dc4b1170c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/c55269cb06911575a126dc225a05c0e4626e5fb4",
"reference": "c55269cb06911575a126dc225a05c0e4626e5fb4",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/418cd304e8e6b417ea79c3b29126a25dc4b1170c",
"reference": "418cd304e8e6b417ea79c3b29126a25dc4b1170c",
"shasum": ""
},
"require": {
@ -4041,20 +4041,23 @@
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.13",
"maennchen/zipstream-php": "^2.1",
"markbaker/complex": "^1.5||^2.0",
"markbaker/matrix": "^1.2||^2.0",
"php": "^7.2||^8.0",
"markbaker/complex": "^2.0",
"markbaker/matrix": "^2.0",
"php": "^7.2 || ^8.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0"
},
"require-dev": {
"dompdf/dompdf": "^0.8.5",
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"dompdf/dompdf": "^1.0",
"friendsofphp/php-cs-fixer": "^2.18",
"jpgraph/jpgraph": "^4.0",
"mpdf/mpdf": "^8.0",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^8.5||^9.3",
"phpstan/phpstan": "^0.12.82",
"phpstan/phpstan-phpunit": "^0.12.18",
"phpunit/phpunit": "^8.5",
"squizlabs/php_codesniffer": "^3.5",
"tecnickcom/tcpdf": "^6.3"
},
@ -4108,9 +4111,9 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.17.1"
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.18.0"
},
"time": "2021-03-02T17:54:11+00:00"
"time": "2021-05-31T18:21:15+00:00"
},
{
"name": "phpspec/prophecy",
@ -4478,16 +4481,16 @@
},
{
"name": "phpunit/phpunit",
"version": "8.5.15",
"version": "8.5.16",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "038d4196d8e8cb405cd5e82cedfe413ad6eef9ef"
"reference": "cc66f2fc61296be66c99931a862200e7456b9a01"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/038d4196d8e8cb405cd5e82cedfe413ad6eef9ef",
"reference": "038d4196d8e8cb405cd5e82cedfe413ad6eef9ef",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/cc66f2fc61296be66c99931a862200e7456b9a01",
"reference": "cc66f2fc61296be66c99931a862200e7456b9a01",
"shasum": ""
},
"require": {
@ -4559,7 +4562,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.15"
"source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.16"
},
"funding": [
{
@ -4571,7 +4574,7 @@
"type": "github"
}
],
"time": "2021-03-17T07:27:54+00:00"
"time": "2021-06-05T04:46:20+00:00"
},
{
"name": "psr/container",
@ -5936,16 +5939,16 @@
},
{
"name": "symfony/process",
"version": "v4.4.22",
"version": "v4.4.25",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "f5481b22729d465acb1cea3455fc04ce84b0148b"
"reference": "cd61e6dd273975c6625316de9d141ebd197f93c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/f5481b22729d465acb1cea3455fc04ce84b0148b",
"reference": "f5481b22729d465acb1cea3455fc04ce84b0148b",
"url": "https://api.github.com/repos/symfony/process/zipball/cd61e6dd273975c6625316de9d141ebd197f93c9",
"reference": "cd61e6dd273975c6625316de9d141ebd197f93c9",
"shasum": ""
},
"require": {
@ -5977,7 +5980,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v4.4.22"
"source": "https://github.com/symfony/process/tree/v4.4.25"
},
"funding": [
{
@ -5993,7 +5996,7 @@
"type": "tidelift"
}
],
"time": "2021-04-07T16:22:29+00:00"
"time": "2021-05-26T11:20:16+00:00"
},
{
"name": "theseer/tokenizer",
@ -8714,16 +8717,16 @@
},
{
"name": "softcreatr/jsonpath",
"version": "0.7.4",
"version": "0.7.5",
"source": {
"type": "git",
"url": "https://github.com/SoftCreatR/JSONPath.git",
"reference": "e01ff3eae8d0b94648354cb02b614968e83d56a2"
"reference": "008569bf80aa3584834f7890781576bc7b65afa7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/e01ff3eae8d0b94648354cb02b614968e83d56a2",
"reference": "e01ff3eae8d0b94648354cb02b614968e83d56a2",
"url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/008569bf80aa3584834f7890781576bc7b65afa7",
"reference": "008569bf80aa3584834f7890781576bc7b65afa7",
"shasum": ""
},
"require": {
@ -8775,7 +8778,7 @@
"type": "github"
}
],
"time": "2021-05-07T14:31:33+00:00"
"time": "2021-06-02T22:15:26+00:00"
},
{
"name": "squizlabs/php_codesniffer",
@ -8895,16 +8898,16 @@
},
{
"name": "symfony/console",
"version": "v4.4.24",
"version": "v4.4.25",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "1b15ca1b1bedda86f98064da9ff5d800560d4c6d"
"reference": "a62acecdf5b50e314a4f305cd01b5282126f3095"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/1b15ca1b1bedda86f98064da9ff5d800560d4c6d",
"reference": "1b15ca1b1bedda86f98064da9ff5d800560d4c6d",
"url": "https://api.github.com/repos/symfony/console/zipball/a62acecdf5b50e314a4f305cd01b5282126f3095",
"reference": "a62acecdf5b50e314a4f305cd01b5282126f3095",
"shasum": ""
},
"require": {
@ -8964,7 +8967,7 @@
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/console/tree/v4.4.24"
"source": "https://github.com/symfony/console/tree/v4.4.25"
},
"funding": [
{
@ -8980,20 +8983,20 @@
"type": "tidelift"
}
],
"time": "2021-05-13T06:28:07+00:00"
"time": "2021-05-26T11:20:16+00:00"
},
{
"name": "symfony/css-selector",
"version": "v4.4.24",
"version": "v4.4.25",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "947cacaf1b3a2af6f13a435392873d5ddaba5f70"
"reference": "c1e29de6dc893b130b45d20d8051efbb040560a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/947cacaf1b3a2af6f13a435392873d5ddaba5f70",
"reference": "947cacaf1b3a2af6f13a435392873d5ddaba5f70",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/c1e29de6dc893b130b45d20d8051efbb040560a9",
"reference": "c1e29de6dc893b130b45d20d8051efbb040560a9",
"shasum": ""
},
"require": {
@ -9029,7 +9032,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v4.4.24"
"source": "https://github.com/symfony/css-selector/tree/v4.4.25"
},
"funding": [
{
@ -9045,7 +9048,7 @@
"type": "tidelift"
}
],
"time": "2021-05-16T09:52:47+00:00"
"time": "2021-05-26T17:39:37+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -9116,16 +9119,16 @@
},
{
"name": "symfony/dom-crawler",
"version": "v4.4.24",
"version": "v4.4.25",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
"reference": "fc0bd1f215b0cd9f4efdc63bb66808f3417331bc"
"reference": "41d15bb6d6b95d2be763c514bb2494215d9c5eef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/fc0bd1f215b0cd9f4efdc63bb66808f3417331bc",
"reference": "fc0bd1f215b0cd9f4efdc63bb66808f3417331bc",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/41d15bb6d6b95d2be763c514bb2494215d9c5eef",
"reference": "41d15bb6d6b95d2be763c514bb2494215d9c5eef",
"shasum": ""
},
"require": {
@ -9169,7 +9172,7 @@
"description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/dom-crawler/tree/v4.4.24"
"source": "https://github.com/symfony/dom-crawler/tree/v4.4.25"
},
"funding": [
{
@ -9185,20 +9188,20 @@
"type": "tidelift"
}
],
"time": "2021-05-16T09:52:47+00:00"
"time": "2021-05-26T11:20:16+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v4.4.20",
"version": "v4.4.25",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "c352647244bd376bf7d31efbd5401f13f50dad0c"
"reference": "047773e7016e4fd45102cedf4bd2558ae0d0c32f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c352647244bd376bf7d31efbd5401f13f50dad0c",
"reference": "c352647244bd376bf7d31efbd5401f13f50dad0c",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/047773e7016e4fd45102cedf4bd2558ae0d0c32f",
"reference": "047773e7016e4fd45102cedf4bd2558ae0d0c32f",
"shasum": ""
},
"require": {
@ -9252,7 +9255,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v4.4.20"
"source": "https://github.com/symfony/event-dispatcher/tree/v4.4.25"
},
"funding": [
{
@ -9268,7 +9271,7 @@
"type": "tidelift"
}
],
"time": "2021-01-27T09:09:26+00:00"
"time": "2021-05-26T17:39:37+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@ -9351,16 +9354,16 @@
},
{
"name": "symfony/finder",
"version": "v4.4.24",
"version": "v4.4.25",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "a96bc19ed87c88eec78e1a4c803bdc1446952983"
"reference": "ed33314396d968a8936c95f5bd1b88bd3b3e94a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/a96bc19ed87c88eec78e1a4c803bdc1446952983",
"reference": "a96bc19ed87c88eec78e1a4c803bdc1446952983",
"url": "https://api.github.com/repos/symfony/finder/zipball/ed33314396d968a8936c95f5bd1b88bd3b3e94a3",
"reference": "ed33314396d968a8936c95f5bd1b88bd3b3e94a3",
"shasum": ""
},
"require": {
@ -9392,7 +9395,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v4.4.24"
"source": "https://github.com/symfony/finder/tree/v4.4.25"
},
"funding": [
{
@ -9408,7 +9411,7 @@
"type": "tidelift"
}
],
"time": "2021-05-16T12:27:45+00:00"
"time": "2021-05-26T11:20:16+00:00"
},
{
"name": "symfony/polyfill-php73",
@ -9653,16 +9656,16 @@
},
{
"name": "symfony/yaml",
"version": "v4.4.24",
"version": "v4.4.25",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "8b6d1b97521e2f125039b3fcb4747584c6dfa0ef"
"reference": "81cdac5536925c1c4b7b50aabc9ff6330b9eb5fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/8b6d1b97521e2f125039b3fcb4747584c6dfa0ef",
"reference": "8b6d1b97521e2f125039b3fcb4747584c6dfa0ef",
"url": "https://api.github.com/repos/symfony/yaml/zipball/81cdac5536925c1c4b7b50aabc9ff6330b9eb5fc",
"reference": "81cdac5536925c1c4b7b50aabc9ff6330b9eb5fc",
"shasum": ""
},
"require": {
@ -9704,7 +9707,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v4.4.24"
"source": "https://github.com/symfony/yaml/tree/v4.4.25"
},
"funding": [
{
@ -9720,7 +9723,7 @@
"type": "tidelift"
}
],
"time": "2021-05-16T09:52:47+00:00"
"time": "2021-05-26T17:39:37+00:00"
},
{
"name": "yiisoft/yii2-debug",

View File

@ -0,0 +1,27 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\assets;
use humhub\components\assets\WebStaticAssetBundle;
use yii\web\View;
class DirectoryAsset extends WebStaticAssetBundle
{
/**
* @inheritdoc
*/
public $js = [
'js/humhub/humhub.directory.js',
];
/**
* @inheritdoc
*/
public $jsOptions = ['position' => View::POS_END];
}

View File

@ -0,0 +1,69 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\admin\controllers;
use humhub\modules\admin\components\Controller;
use humhub\modules\admin\models\forms\PeopleSettingsForm;
use humhub\modules\admin\permissions\ManageSettings;
use Yii;
use yii\web\HttpException;
/**
* User People Configuration
*
* @since 1.9
*/
class UserPeopleController extends Controller
{
/**
* @inheritdoc
*/
public $adminOnly = false;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->appendPageTitle(Yii::t('AdminModule.base', 'People'));
$this->subLayout = '@admin/views/layouts/user';
}
/**
* @inheritdoc
*/
public function getAccessRules()
{
return [
['permissions' => [ManageSettings::class]]
];
}
/**
* Configuration for People page
*
* @return string
* @throws HttpException
*/
public function actionIndex()
{
$form = new PeopleSettingsForm();
if ($form->load(Yii::$app->request->post()) && $form->validate() && $form->save()) {
$this->view->saved();
return $this->redirect(['/admin/user-people']);
}
return $this->render('index', [
'model' => $form,
]);
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\admin\models\forms;
use humhub\modules\user\models\ProfileField;
use humhub\modules\user\models\ProfileFieldCategory;
use Yii;
use humhub\libs\DynamicConfig;
use yii\base\Model;
/**
* PeopleSettingsForm
* @since 1.9
*/
class PeopleSettingsForm extends Model
{
public $detail1;
public $detail2;
public $detail3;
public $defaultSorting;
/**
* @var array Cached options for card details from tables of user profile and its categories
*/
private $detailOptions;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
// Set default values
$this->detail1 = Yii::$app->settings->get('people.detail1', '');
$this->detail2 = Yii::$app->settings->get('people.detail2', '');
$this->detail3 = Yii::$app->settings->get('people.detail3', '');
$this->defaultSorting = Yii::$app->settings->get('people.defaultSorting', 'lastlogin');
}
/**
* @inheritdoc
*/
public function rules()
{
return [
['detail1', 'in', 'range' => $this->getDetailKeys()],
['detail2', 'in', 'range' => $this->getDetailKeys()],
['detail3', 'in', 'range' => $this->getDetailKeys()],
['defaultSorting', 'in', 'range' => array_keys(self::getSortingOptions())],
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'detail1' => Yii::t('AdminModule.user', 'Information 1'),
'detail2' => Yii::t('AdminModule.user', 'Information 2'),
'detail3' => Yii::t('AdminModule.user', 'Information 3'),
'defaultSorting' => Yii::t('AdminModule.user', 'Default Sorting'),
];
}
/**
* Saves the form
* @return boolean
*/
public function save()
{
Yii::$app->settings->set('people.detail1', $this->detail1);
Yii::$app->settings->set('people.detail2', $this->detail2);
Yii::$app->settings->set('people.detail3', $this->detail3);
Yii::$app->settings->set('people.defaultSorting', $this->defaultSorting);
return true;
}
public function getDetailOptions(): array
{
if (isset($this->detailOptions)) {
return $this->detailOptions;
}
$this->detailOptions = ['' => Yii::t('AdminModule.user', 'None')];
$profileFields = ProfileField::find()
->leftJoin('profile_field_category', 'profile_field_category.id = profile_field_category_id')
->orderBy('profile_field_category.sort_order, profile_field.sort_order')
->all();
foreach ($profileFields as $profileField) {
/* @var $profileField ProfileField */
/* @var $profileFieldCategory ProfileFieldCategory */
$profileFieldCategory = $profileField->getCategory()->one();
if (!isset($this->detailOptions[$profileFieldCategory->title])) {
$this->detailOptions[$profileFieldCategory->title] = [];
}
$this->detailOptions[$profileFieldCategory->title][$profileField->internal_name] = $profileField->title . ($profileField->visible ? '' : ' (' . Yii::t('AdminModule.user', 'Not visible') . ')');
}
return $this->detailOptions;
}
private function getDetailKeys(): array
{
$keys = [];
$options = self::getDetailOptions();
foreach ($options as $key => $option) {
if (is_array($option)) {
$keys = array_merge($keys, array_keys($option));
} else {
$keys[] = $key;
}
}
return $keys;
}
public static function getSortingOptions(): array
{
return [
'firstname' => Yii::t('AdminModule.user', 'First name'),
'lastname' => Yii::t('AdminModule.user', 'Last name'),
'lastlogin' => Yii::t('AdminModule.user', 'Last login'),
];
}
}

View File

@ -20,7 +20,7 @@ class MaintenanceModeCest
$I->amGoingTo('try to login with correct credentials');
$loginPage->login('Admin', 'test');
$I->expectTo('see dashboard');
$I->waitForText('DIRECTORY');
$I->waitForText('DASHBOARD');
$I->dontSee('Administration');
}

View File

@ -0,0 +1,36 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\libs\Html;
use humhub\modules\admin\models\forms\PeopleSettingsForm;
use humhub\modules\ui\form\widgets\ActiveForm;
use humhub\widgets\Button;
/* @var $model PeopleSettingsForm */
?>
<div class="panel-body">
<h4><?= Yii::t('AdminModule.user', 'People'); ?></h4>
<div class="help-block">
<?= Yii::t('AdminModule.user', 'Select which user information should be displayed in the \'People\' overview. You can select any profile fields, even those you have created individually. '); ?>
</div>
<br />
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'detail1')->dropDownList($model->getDetailOptions()); ?>
<?= $form->field($model, 'detail2')->dropDownList($model->getDetailOptions()); ?>
<?= $form->field($model, 'detail3')->dropDownList($model->getDetailOptions()); ?>
<?= $form->field($model, 'defaultSorting')->dropDownList(PeopleSettingsForm::getSortingOptions()); ?>
<?= Button::save(Yii::t('AdminModule.user', 'Save'))->submit(); ?>
<?php ActiveForm::end(); ?>
</div>

View File

@ -67,7 +67,7 @@ class AdminMenu extends LeftNavigation
'url' => ['/admin/user'],
'icon' => 'user',
'sortOrder' => 200,
'isActive' => MenuLink::isActiveState('admin', ['user', 'group', 'approval', 'authentication', 'user-profile', 'pending-registrations', 'user-permissions']) ||
'isActive' => MenuLink::isActiveState('admin', ['user', 'group', 'approval', 'authentication', 'user-profile', 'pending-registrations', 'user-permissions', 'user-people']) ||
MenuLink::isActiveState('ldap', 'admin'),
'isVisible' => Yii::$app->user->can([
ManageUsers::class,

View File

@ -80,6 +80,14 @@ class UserMenu extends TabMenu
'isVisible' => Yii::$app->user->can(ManageGroups::class)
]));
$this->addEntry(new MenuLink([
'label' => Yii::t('AdminModule.user', 'People'),
'url' => ['/admin/user-people'],
'sortOrder' => 600,
'isActive' => MenuLink::isActiveState('admin', 'user-people'),
'isVisible' => Yii::$app->user->can(ManageSettings::class)
]));
parent::init();
}

View File

@ -36,6 +36,23 @@ humhub.module('content.container', function (module, require, $) {
});
};
var relationship = function(evt) {
var postOptions = {};
var buttonOptions = evt.$trigger.data('button-options');
if (buttonOptions) {
postOptions.data = {options: buttonOptions};
}
client.post(evt, postOptions).then(function(response) {
var oldButton = evt.$trigger;
if (oldButton.closest('.btn-group').length) {
oldButton = oldButton.closest('.btn-group');
}
oldButton.replaceWith(response.data);
}).catch(function(e) {
module.log.error(e, true);
});
}
var enableModule = function (evt) {
client.post(evt).then(function (response) {
if (response.success) {
@ -79,6 +96,7 @@ humhub.module('content.container', function (module, require, $) {
module.export({
follow: follow,
unfollow: unfollow,
relationship: relationship,
unload: unload,
guid: guid,
enableModule: enableModule,

View File

@ -10,7 +10,6 @@ namespace humhub\modules\directory;
use humhub\modules\ui\menu\MenuLink;
use Yii;
use yii\helpers\Url;
use humhub\modules\user\models\Group;
use humhub\modules\directory\permissions\AccessDirectory;
@ -22,6 +21,7 @@ use humhub\modules\directory\permissions\AccessDirectory;
*
* @package humhub.modules_core.directory
* @since 0.5
* @deprecated since 1.9 but it can be activated temporary by console command `php yii directory/activate`
*/
class Module extends \humhub\components\Module
{
@ -44,7 +44,7 @@ class Module extends \humhub\components\Module
/**
* @var bool defines if the directory is active, if not the directory is not visible and can't be accessed
*/
public $active = true;
public $active = false;
/**
* @var bool defines if the directory is available for guest users, this flag will only have effect if guest access is allowed and the module is active
@ -56,6 +56,15 @@ class Module extends \humhub\components\Module
*/
public $showUserProfilePosts = true;
/**
* @inerhitdoc
*/
public function init()
{
parent::init();
$this->active = $this->settings->get('isActive', false);
}
/**
* @return bool checks if the current user can access the directory
@ -101,7 +110,7 @@ class Module extends \humhub\components\Module
*/
public function getPermissions($contentContainer = null)
{
if (!$contentContainer) {
if ($this->active && !$contentContainer) {
return [
new AccessDirectory(),
];

View File

@ -0,0 +1,46 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\directory\commands;
use humhub\components\SettingsManager;
use humhub\modules\directory\Module;
use Yii;
/**
* Management of activity of the deprecated module "Directory"
*
* @since 1.9
*/
class DirectoryController extends \yii\console\Controller
{
/**
* Activate the deprecated module "Directory"
*/
public function actionActivate()
{
$this->getModuleSettings()->set('isActive', true);
$this->stdout('Module "Directory" is activated.' . "\n\n");
}
/**
* Deactivate the deprecated module "Directory"
*/
public function actionDeactivate()
{
$this->getModuleSettings()->delete('isActive');
$this->stdout('Module "Directory" is deactivated.' . "\n\n");
}
protected function getModuleSettings(): SettingsManager
{
/* @var Module $module */
$module = Yii::$app->getModule('directory');
return $module->settings;
}
}

View File

@ -7,6 +7,7 @@ return [
'id' => 'directory',
'class' => Module::class,
'isCoreModule' => true,
'consoleControllerMap' => ['directory' => 'humhub\modules\directory\commands\DirectoryController'],
'events' => [
['class' => TopMenu::class, 'event' => TopMenu::EVENT_INIT, 'callback' => [Module::class, 'onTopMenuInit']],
],

View File

@ -11,6 +11,7 @@ namespace humhub\modules\friendship\controllers;
use humhub\components\Controller;
use humhub\modules\friendship\models\Friendship;
use humhub\modules\friendship\Module;
use humhub\modules\friendship\widgets\FriendshipButton;
use humhub\modules\user\models\User;
use Yii;
use yii\web\HttpException;
@ -44,17 +45,11 @@ class RequestController extends Controller
*/
public function actionAdd()
{
$this->forcePostRequest();
$friend = User::findOne(['id' => Yii::$app->request->get('userId')]);
if ($friend === null) {
throw new HttpException(404, 'User not found!');
}
$friend = $this->getFriendUser();
Friendship::add(Yii::$app->user->getIdentity(), $friend);
return $this->redirect($friend->getUrl());
return $this->getActionResult($friend);
}
/**
@ -62,6 +57,21 @@ class RequestController extends Controller
* @throws HttpException
*/
public function actionDelete()
{
$friend = $this->getFriendUser();
Friendship::cancel(Yii::$app->user->getIdentity(), $friend);
return $this->getActionResult($friend);
}
/**
* Get friend User from request
*
* @return User
* @throws HttpException
*/
protected function getFriendUser(): User
{
$this->forcePostRequest();
@ -71,9 +81,26 @@ class RequestController extends Controller
throw new HttpException(404, 'User not found!');
}
Friendship::cancel(Yii::$app->user->getIdentity(), $friend);
return $friend;
}
return $this->redirect($friend->getUrl());
/**
* Get result for the friendship actions
*
* @param User $user
* @return string|\yii\console\Response|\yii\web\Response
* @throws \Exception
*/
protected function getActionResult(User $user)
{
if ($this->request->isAjax) {
return FriendshipButton::widget([
'user' => $user,
'options' => $this->request->post('options', []),
]);
}
return $this->redirect($this->request->getReferrer());
}
}

View File

@ -8,8 +8,12 @@
namespace humhub\modules\friendship\widgets;
use Yii;
use humhub\modules\friendship\models\Friendship;
use humhub\modules\user\models\User;
use Yii;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;
use yii\helpers\Url;
/**
* Displays a membership button between the current and given user.
@ -20,10 +24,90 @@ class FriendshipButton extends \yii\base\Widget
{
/**
* @var User the target user
* @var User the target user
*/
public $user;
/**
* @var array Options buttons
*/
public $options = [];
private function getDefaultOptions()
{
return [
'friends' => [
'title' => '<span class="glyphicon glyphicon-ok"></span>&nbsp;&nbsp;' . Yii::t('FriendshipModule.base', 'Friends'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => Url::to(['/friendship/request/delete', 'userId' => $this->user->id]),
'data-action-confirm' => Yii::t('FriendshipModule.base', 'Would you like to end your friendship with {userName}?', ['{userName}' => '<strong>' . $this->user->getDisplayName() . '</strong>']),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
'class' => 'btn btn-info active',
],
],
'addFriend' => [
'title' => '<span class="glyphicon glyphicon-plus"></span>&nbsp;&nbsp;' . Yii::t('FriendshipModule.base', 'Friends'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => Url::to(['/friendship/request/add', 'userId' => $this->user->id]),
'data-action-confirm' => Yii::t('FriendshipModule.base', 'Would you like to send a friendship request to {userName}?', ['{userName}' => '<strong>' . $this->user->getDisplayName() . '</strong>']),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
'class' => 'btn btn-info',
],
],
'acceptFriendRequest' => [
'title' => '<span class="glyphicon glyphicon-time"></span>&nbsp;&nbsp;' . Yii::t('FriendshipModule.base', 'Accept Friend Request'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => Url::to(['/friendship/request/add', 'userId' => $this->user->id]),
'data-action-confirm' => Yii::t('FriendshipModule.base', 'Would you like to accept the friendship request?'),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
'class' => 'btn btn-info active',
],
'groupClass' => 'btn-group',
'togglerClass' => 'btn btn-info active',
],
'denyFriendRequest' => [
'title' => '<span class="fa fa-times"></span>&nbsp;&nbsp;' . Yii::t('FriendshipModule.base', 'Deny friend request'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => Url::to(['/friendship/request/delete', 'userId' => $this->user->id]),
'data-action-confirm' => Yii::t('FriendshipModule.base', 'Would you like to withdraw the friendship request?'),
'data-button-options' => Json::encode($this->options),
],
],
'cancelFriendRequest' => [
'title' => '<span class="glyphicon glyphicon-time"></span>&nbsp;&nbsp;' . Yii::t('FriendshipModule.base', 'Pending'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => Url::to(['/friendship/request/delete', 'userId' => $this->user->id]),
'data-action-confirm' => Yii::t('FriendshipModule.base', 'Would you like to withdraw your friendship request?'),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
'class' => 'btn btn-info active',
],
],
];
}
public function setDefaultOptions(array $defaultOptions)
{
$this->options = $this->getOptions($defaultOptions);
}
public function getOptions(array $defaultOptions = null): array
{
if ($defaultOptions === null) {
$defaultOptions = $this->getDefaultOptions();
}
return ArrayHelper::merge($defaultOptions, $this->options);
}
/**
* @inheritdoc
*/
@ -34,13 +118,14 @@ class FriendshipButton extends \yii\base\Widget
}
// Do not display a buttton if user is it self or guest
if ($this->user->isCurrentUser() || \Yii::$app->user->isGuest) {
if ($this->user->isCurrentUser() || Yii::$app->user->isGuest) {
return;
}
return $this->render('friendshipButton', [
'user' => $this->user,
'friendshipState' => Friendship::getStateForUser(Yii::$app->user->getIdentity(), $this->user)
'user' => $this->user,
'friendshipState' => Friendship::getStateForUser(Yii::$app->user->getIdentity(), $this->user),
'options' => $this->getOptions(),
]);
}

View File

@ -1,36 +1,29 @@
<?php
use yii\helpers\Html;
use yii\helpers\Url;
use humhub\modules\friendship\models\Friendship;
/* @var $user \humhub\modules\user\models\User */
/* @var $friendshipState string */
/* @var $options array */
?>
<?php if ($friendshipState === Friendship::STATE_FRIENDS) : ?>
<div class="btn-group">
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-ok"></span> <?= Yii::t("FriendshipModule.base", "Friends"); ?>&nbsp;<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><?= Html::a(Yii::t("FriendshipModule.base", "Unfriend"), Url::to(['/friendship/request/delete', 'userId' => $user->id]), ['data-method' => 'POST', 'data-ui-loader' => '']); ?></li>
</ul>
</div>
<?= Html::a($options['friends']['title'], '#', $options['friends']['attrs']); ?>
<?php elseif ($friendshipState === Friendship::STATE_NONE) : ?>
<?= Html::a('<span class="glyphicon glyphicon-plus"></span>&nbsp;&nbsp;' . Yii::t("FriendshipModule.base", "Add Friend"), Url::to(['/friendship/request/add', 'userId' => $user->id]), ['class' => 'btn btn-info', 'data-method' => 'POST', 'data-ui-loader' => '']); ?>
<?= Html::a($options['addFriend']['title'], '#', $options['addFriend']['attrs']); ?>
<?php elseif ($friendshipState === Friendship::STATE_REQUEST_RECEIVED) : ?>
<div class="btn-group">
<?= Html::a(Yii::t("FriendshipModule.base", "Accept Friend Request"), Url::to(['/friendship/request/add', 'userId' => $user->id]), ['class' => 'btn btn-success', 'data-method' => 'POST', 'data-ui-loader' => '']); ?>
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<div class="<?= $options['acceptFriendRequest']['groupClass'] ?>">
<?= Html::a($options['acceptFriendRequest']['title'], '#', $options['acceptFriendRequest']['attrs']); ?>
<button type="button" class="<?= $options['acceptFriendRequest']['togglerClass'] ?> dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li>
<?= Html::a(Yii::t("FriendshipModule.base", "Deny friend request"), Url::to(['/friendship/request/delete', 'userId' => $user->id]), ['data-method' => 'POST', 'data-ui-loader' => '']); ?>
<?= Html::a($options['denyFriendRequest']['title'], '#', $options['denyFriendRequest']['attrs']); ?>
</li>
</ul>
</div>
<?php elseif ($friendshipState === Friendship::STATE_REQUEST_SENT) : ?>
<?= Html::a(Yii::t("FriendshipModule.base", "Cancel friend request"), Url::to(['/friendship/request/delete', 'userId' => $user->id]), ['class' => 'btn btn-danger', 'data-method' => 'POST', 'data-ui-loader' => '']); ?>
<?= Html::a($options['cancelFriendRequest']['title'], '#', $options['cancelFriendRequest']['attrs']); ?>
<?php endif; ?>

View File

@ -8,6 +8,8 @@
namespace humhub\modules\space;
use humhub\modules\space\permissions\SpaceDirectoryAccess;
use humhub\modules\ui\menu\MenuLink;
use humhub\modules\user\events\UserEvent;
use humhub\modules\space\models\Space;
use humhub\modules\space\models\Membership;
@ -98,4 +100,29 @@ class Events extends BaseObject
}
}
/**
* On build of the TopMenu
*
* @param Event $event
*/
public static function onTopMenuInit($event)
{
if (Yii::$app->user->isGuest) {
return;
}
if (!Yii::$app->user->can(SpaceDirectoryAccess::class)) {
return;
}
$event->sender->addEntry(new MenuLink([
'id' => 'spaces',
'icon' => 'dot-circle-o',
'label' => Yii::t('SpaceModule.base', 'Spaces'),
'url' => ['/space/spaces'],
'sortOrder' => 250,
'isActive' => MenuLink::isActiveState('space', 'spaces'),
]));
}
}

View File

@ -80,6 +80,7 @@ class Module extends \humhub\components\Module
}
return [
new permissions\SpaceDirectoryAccess(),
new permissions\CreatePrivateSpace(),
new permissions\CreatePublicSpace(),
];

View File

@ -0,0 +1,136 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\space\components;
use humhub\modules\space\models\Membership;
use humhub\modules\space\models\Space;
use humhub\modules\space\widgets\SpaceDirectoryFilters;
use Yii;
use yii\data\Pagination;
/**
* SpaceDirectoryQuery is used to query Space records on the Spaces page.
*
* @author luke
*/
class SpaceDirectoryQuery extends ActiveQuerySpace
{
/**
* @var Pagination
*/
public $pagination;
/**
* @var int
*/
public $pageSize = 18;
/**
* @inheritdoc
*/
public function __construct($config = [])
{
parent::__construct(Space::class, $config);
}
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->visible();
$this->filterByKeyword();
$this->filterByConnection();
$this->order();
$this->paginate();
}
public function filterByKeyword(): SpaceDirectoryQuery
{
$keyword = Yii::$app->request->get('keyword', '');
return $this->search($keyword);
}
public function filterByConnection(): SpaceDirectoryQuery
{
switch (Yii::$app->request->get('connection')) {
case 'member':
return $this->filterByConnectionMember();
case 'follow':
return $this->filterByConnectionFollow();
case 'none':
return $this->filterByConnectionNone();
}
return $this;
}
public function filterByConnectionMember(): SpaceDirectoryQuery
{
return $this->innerJoin('space_membership', 'space_membership.space_id = space.id')
->andWhere(['space_membership.user_id' => Yii::$app->user->id])
->andWhere(['space_membership.status' => Membership::STATUS_MEMBER]);
}
public function filterByConnectionFollow(): SpaceDirectoryQuery
{
return $this->innerJoin('user_follow', 'user_follow.object_model = :spaceClass AND user_follow.object_id = space.id', [':spaceClass' => Space::class])
->andWhere(['user_follow.user_id' => Yii::$app->user->id]);
}
public function filterByConnectionNone(): SpaceDirectoryQuery
{
return $this->andWhere('space.id NOT IN (SELECT space_id FROM space_membership WHERE user_id = :userId AND status = :memberStatus)')
->andWhere('space.id NOT IN (SELECT object_id FROM user_follow WHERE user_id = :userId AND user_follow.object_model = :spaceClass)')
->addParams([
':userId' => Yii::$app->user->id,
':memberStatus' => Membership::STATUS_MEMBER,
':spaceClass' => Space::class,
]);
}
public function order(): SpaceDirectoryQuery
{
switch (SpaceDirectoryFilters::getValue('sort')) {
case 'name':
$this->addOrderBy('space.name');
break;
case 'newer':
$this->addOrderBy('space.created_at DESC');
break;
case 'older':
$this->addOrderBy('space.created_at');
break;
}
return $this;
}
public function paginate(): SpaceDirectoryQuery
{
$countQuery = clone $this;
$this->pagination = new Pagination(['totalCount' => $countQuery->count(), 'pageSize' => $this->pageSize]);
return $this->offset($this->pagination->offset)->limit($this->pagination->limit);
}
public function isLastPage(): bool
{
return $this->pagination->getPage() == $this->pagination->getPageCount() - 1;
}
}

View File

@ -4,15 +4,16 @@ use humhub\modules\search\engine\Search;
use humhub\modules\user\models\User;
use humhub\modules\space\Events;
use humhub\modules\space\Module;
use humhub\components\console\Application;
use humhub\commands\IntegrityController;
use humhub\widgets\TopMenu;
return [
'id' => 'space',
'class' => Module::class,
'isCoreModule' => true,
'urlManagerRules' => [
['class' => 'humhub\modules\space\components\UrlRule']
['class' => 'humhub\modules\space\components\UrlRule'],
'spaces' => 'space/spaces',
],
'modules' => [
'manage' => [
@ -26,5 +27,6 @@ return [
[User::class, User::EVENT_BEFORE_SOFT_DELETE, [Events::class, 'onUserSoftDelete']],
[Search::class, Search::EVENT_ON_REBUILD, [Events::class, 'onSearchRebuild']],
[IntegrityController::class, IntegrityController::EVENT_ON_RUN, [Events::class, 'onIntegrityCheck']],
[TopMenu::class, TopMenu::EVENT_INIT, [Events::class, 'onTopMenuInit']],
],
];

View File

@ -18,10 +18,12 @@ use humhub\modules\space\models\forms\RequestMembershipForm;
use humhub\modules\space\models\Membership;
use humhub\modules\space\models\Space;
use humhub\modules\space\permissions\InviteUsers;
use humhub\modules\space\widgets\MembershipButton;
use humhub\modules\user\models\UserPicker;
use humhub\modules\user\widgets\UserListBox;
use humhub\widgets\ModalClose;
use Yii;
use yii\helpers\Json;
use yii\web\HttpException;
/**
@ -94,7 +96,7 @@ class MembershipController extends ContentContainerController
$space->addMember(Yii::$app->user->id);
return $this->htmlRedirect($space->getUrl());
return $this->getActionResult($space);
}
/**
@ -116,9 +118,18 @@ class MembershipController extends ContentContainerController
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
$space->requestMembership(Yii::$app->user->id, $model->message);
return $this->renderAjax('requestMembershipSave', ['space' => $space]);
return $this->renderAjax('requestMembershipSave', [
'spaceId' => $space->id,
'newMembershipButton' => MembershipButton::widget([
'space' => $space,
'options' => empty($model->options) ? [] : Json::decode($model->options)
]),
]);
}
$model->options = $this->request->get('options');
return $this->renderAjax('requestMembership', ['model' => $model, 'space' => $space]);
}
@ -160,7 +171,7 @@ class MembershipController extends ContentContainerController
$space->removeMember();
return $this->goHome();
return $this->getActionResult($space);
}
/**
@ -225,7 +236,7 @@ class MembershipController extends ContentContainerController
$space->addMember(Yii::$app->user->id);
}
return $this->redirect($space->getUrl());
return $this->getActionResult($space);
}
/**
@ -256,4 +267,23 @@ class MembershipController extends ContentContainerController
]));
}
/**
* Get result for the membership actions
*
* @param Space $space
* @return string|\yii\console\Response|\yii\web\Response
* @throws \Exception
*/
protected function getActionResult(Space $space)
{
if ($this->request->isAjax) {
return MembershipButton::widget([
'space' => $space,
'options' => $this->request->post('options', []),
]);
}
return $this->redirect($this->request->getReferrer());
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\space\controllers;
use humhub\components\access\ControllerAccess;
use humhub\components\Controller;
use humhub\modules\space\components\SpaceDirectoryQuery;
use humhub\modules\space\permissions\SpaceDirectoryAccess;
use humhub\modules\space\widgets\SpaceDirectoryCard;
use Yii;
use yii\helpers\Url;
/**
* SpacesController displays users directory
*
* @since 1.9
*/
class SpacesController extends Controller
{
/**
* @inheritdoc
*/
public $subLayout = '@space/views/spaces/_layout';
/**
* @inheritdoc
*/
public function init()
{
$this->setActionTitles([
'index' => Yii::t('SpaceModule.base', 'Spaces'),
]);
parent::init();
}
/**
* @inheritdoc
*/
public function getAccessRules()
{
return [
[ControllerAccess::RULE_LOGGED_IN_ONLY],
['permissions' => [SpaceDirectoryAccess::class]],
];
}
/**
* Action to display spaces page
*/
public function actionIndex()
{
$spaceDirectoryQuery = new SpaceDirectoryQuery();
$urlParams = Yii::$app->request->getQueryParams();
unset($urlParams['page']);
array_unshift($urlParams, '/space/spaces/load-more');
$this->getView()->registerJsConfig('directory', [
'loadMoreUrl' => Url::to($urlParams),
]);
return $this->render('index', [
'spaces' => $spaceDirectoryQuery,
]);
}
/**
* Action to load cards for next page by AJAX
*/
public function actionLoadMore()
{
$spaceQuery = new SpaceDirectoryQuery();
$spaceCards = '';
foreach ($spaceQuery->all() as $space) {
$spaceCards .= SpaceDirectoryCard::widget(['space' => $space]);
}
return $spaceCards;
}
}

View File

@ -15,6 +15,7 @@ class RequestMembershipForm extends Model
public $space_id;
public $message;
public $options;
/**
* Declares the validation rules.
@ -22,7 +23,8 @@ class RequestMembershipForm extends Model
public function rules()
{
return [
['message', 'required']
['message', 'required'],
['options', 'safe'],
];
}

View File

@ -0,0 +1,40 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\space\permissions;
use humhub\libs\BasePermission;
use Yii;
class SpaceDirectoryAccess extends BasePermission
{
/**
* @inheritdoc
*/
protected $moduleId = 'space';
/**
* @inheritdoc
*/
protected $defaultState = self::STATE_ALLOW;
/**
* @inheritdoc
*/
public function getTitle()
{
return Yii::t('SpaceModule.permissions', 'Can Access Space Directory');
}
/**
* @inheritdoc
*/
public function getDescription()
{
return Yii::t('SpaceModule.permissions', 'Can access the space directory section.');
}
}

View File

@ -21,8 +21,8 @@ class RequestMembershipCest
$I->amUser1();
$I->amOnSpace1();
$I->seeElement('#requestMembershipButton');
$I->click('#requestMembershipButton');
$I->seeElement('[data-space-request-membership]');
$I->click('[data-space-request-membership]');
$I->waitForText('Request space membership', null,'#globalModal');
$I->fillField('#request-message', 'Hi, I want to join this space.');
@ -57,8 +57,8 @@ class RequestMembershipCest
$I->amUser1();
$I->amOnSpace1();
$I->seeElement('#requestMembershipButton');
$I->click('#requestMembershipButton');
$I->seeElement('[data-space-request-membership]');
$I->click('[data-space-request-membership]');
$I->waitForText('Request space membership', null,'#globalModal');
$I->fillField('#request-message', 'Hi, I want to join this space.');
@ -90,7 +90,7 @@ class RequestMembershipCest
$I->amUser1(true);
$I->seeInNotifications('Admin Tester declined your membership request for the space Space 1', true);
$I->waitForElementVisible('#requestMembershipButton');
$I->waitForElementVisible('[data-space-request-membership]');
}
/**
@ -103,8 +103,8 @@ class RequestMembershipCest
$I->amUser1();
$I->amOnSpace1();
$I->seeElement('#requestMembershipButton');
$I->click('#requestMembershipButton');
$I->seeElement('[data-space-request-membership]');
$I->click('[data-space-request-membership]');
$I->waitForText('Request space membership', null,'#globalModal');
$I->fillField('#request-message', 'Hi, I want to join this space.');
@ -116,7 +116,7 @@ class RequestMembershipCest
$I->click('Cancel pending membership application');
$I->waitForText('Admin Space 2 Post Private', null,'#wallStream'); // Back to dashboard
$I->amOnSpace1();
$I->waitForText('Request membership', null,'#requestMembershipButton');
$I->waitForText('Request membership', null,'[data-space-request-membership]');
$I->amAdmin(true);
$I->dontSeeInNotifications('Peter Tester requests membership for the space Space 1');

View File

@ -2,10 +2,16 @@
use humhub\compat\CActiveForm;
use humhub\libs\Html;
use humhub\modules\space\models\forms\RequestMembershipForm;
use humhub\modules\space\models\Space;
/* @var $space Space */
/* @var $model RequestMembershipForm */
?>
<div class="modal-dialog animated fadeIn">
<div class="modal-content">
<?php $form = CActiveForm::begin(); ?>
<?= $form->hiddenField($model, 'options') ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title"
@ -30,7 +36,7 @@ use humhub\libs\Html;
'label' => Yii::t('SpaceModule.base', 'Send'),
'ajaxOptions' => [
'type' => 'POST',
'beforeSend' => new yii\web\JsExpression('function(){ setModalLoader(); }'),
'beforeSend' => new yii\web\JsExpression('function(){ setModalLoader(evt); }'),
'success' => new yii\web\JsExpression('function(html){ $("#globalModal").html(html); }'),
'url' => $space->createUrl('/space/membership/request-membership-form'),
],

View File

@ -1,7 +1,9 @@
<?php
use humhub\modules\space\widgets\MembershipButton;
use humhub\libs\Html;
/* @var $spaceId int */
/* @var $newMembershipButton string */
?>
<div class="modal-dialog animated fadeIn">
@ -28,5 +30,5 @@ use humhub\libs\Html;
</div>
</div>
<script <?= Html::nonce() ?>>
$('#requestMembershipButton').replaceWith('<?= MembershipButton::widget(['space' => $space]) ?>');
$('[data-space-request-membership=<?= $spaceId ?>]').replaceWith('<?= $newMembershipButton ?>');
</script>

View File

@ -0,0 +1,9 @@
<?php
use humhub\modules\ui\view\helpers\ThemeHelper;
/* @var $content string */
?>
<div class="<?php if (ThemeHelper::isFluid()): ?>container-fluid<?php else: ?>container<?php endif; ?> container-directory container-spaces">
<?= $content; ?>
</div>

View File

@ -0,0 +1,59 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\assets\DirectoryAsset;
use humhub\modules\space\components\SpaceDirectoryQuery;
use humhub\modules\space\widgets\SpaceDirectoryCard;
use humhub\modules\space\widgets\SpaceDirectoryFilters;
use humhub\widgets\Button;
use humhub\widgets\LinkPager;
use yii\web\View;
/* @var $this View */
/* @var $spaces SpaceDirectoryQuery */
DirectoryAsset::register($this);
?>
<div class="panel panel-default">
<div class="panel-heading">
<?= Yii::t('SpaceModule.base', '<strong>Spaces</strong> directory'); ?>
</div>
<div class="panel-body">
<?= SpaceDirectoryFilters::widget(); ?>
</div>
</div>
<div class="row cards">
<?php if (!$spaces->exists()): ?>
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-body">
<?= Yii::t('SpaceModule.base', 'No spaces found!'); ?>
</div>
</div>
</div>
<?php endif; ?>
<?php foreach ($spaces->all() as $space) : ?>
<?= SpaceDirectoryCard::widget(['space' => $space]); ?>
<?php endforeach; ?>
</div>
<?php if (!$spaces->isLastPage()) : ?>
<div class="directory-load-more">
<?= Button::primary(Yii::t('SpaceModule.base', 'Load more'))
->icon('fa-angle-down')
->action('directory.loadMore')
->options([
'data-current-page' => $spaces->pagination->getPage() + 1,
'data-total-pages' => $spaces->pagination->getPageCount(),
]) ?>
</div>
<?php endif; ?>

View File

@ -45,7 +45,7 @@ class FollowButton extends Widget
/**
* @var array options for unfollow button
*/
public $unfollowOptions = ['class' => 'btn btn-info btn-sm'];
public $unfollowOptions = ['class' => 'btn btn-primary btn-sm active'];
/**
* @inheritdoc
@ -57,7 +57,7 @@ class FollowButton extends Widget
}
if ($this->unfollowLabel === null) {
$this->unfollowLabel = Yii::t('SpaceModule.base', 'Unfollow');
$this->unfollowLabel = '<span class="glyphicon glyphicon-ok"></span>&nbsp;&nbsp;' . Yii::t('SpaceModule.base', 'Following');
}
if (!isset($this->followOptions['class'])) {
@ -113,6 +113,11 @@ class FollowButton extends Widget
$this->followOptions['data-ui-loader'] = '';
$this->unfollowOptions['data-ui-loader'] = '';
// Confirm action "Unfollow"
$this->unfollowOptions['data-action-confirm'] = Yii::t('SpaceModule.base', 'Would you like to unfollow Space {spaceName}?', [
'{spaceName}' => '<strong>' . $this->space->getDisplayName() . '</strong>'
]);
$module = Yii::$app->getModule('space');
// still enable unfollow if following was disabled afterwards.

View File

@ -9,6 +9,10 @@
namespace humhub\modules\space\widgets;
use humhub\components\Widget;
use humhub\modules\space\models\Space;
use Yii;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;
/**
* MembershipButton shows various membership related buttons in space header.
@ -20,20 +24,115 @@ class MembershipButton extends Widget
{
/**
* @var \humhub\modules\space\models\Space
* @var Space
*/
public $space;
/**
* @var array Options buttons
*/
public $options = [];
private function getDefaultOptions()
{
return [
'requestMembership' => [
'title' => Yii::t('SpaceModule.base', 'Join'),
'url' => $this->space->createUrl('/space/membership/request-membership-form', ['options' => Json::encode($this->options)]),
'attrs' => [
'class' => 'btn btn-info',
'data-space-request-membership' => $this->space->id,
'data-target' => '#globalModal',
],
],
'becomeMember' => [
'title' => Yii::t('SpaceModule.base', 'Join'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => $this->space->createUrl('/space/membership/request-membership'),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
'class' => 'btn btn-info',
'data-space-request-membership' => $this->space->id,
],
],
'acceptInvite' => [
'title' => Yii::t('SpaceModule.base', 'Accept Invite'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => $this->space->createUrl('/space/membership/invite-accept'),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
'class' => 'btn btn-info',
],
'groupClass' => 'btn-group',
'togglerClass' => 'btn btn-info',
],
'declineInvite' => [
'title' => Yii::t('SpaceModule.base', 'Decline Invite'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => $this->space->createUrl('/space/membership/revoke-membership'),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
],
],
'cancelPendingMembership' => [
'title' => '<span class="glyphicon glyphicon-time"></span>&nbsp;&nbsp;' . Yii::t('SpaceModule.base', 'Pending'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => $this->space->createUrl('/space/membership/revoke-membership'),
'data-action-confirm' => Yii::t('SpaceModule.base', 'Would you like to withdraw your request to join Space {spaceName}?', ['{spaceName}' => '<strong>' . $this->space->getDisplayName() . '</strong>']),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
'class' => 'btn btn-info active',
],
],
'cancelMembership' => [
'visible' => false,
'title' => '<span class="glyphicon glyphicon-ok"></span>&nbsp;&nbsp;' . Yii::t('SpaceModule.base', 'Member'),
'attrs' => [
'data-action-click' => 'content.container.relationship',
'data-action-url' => $this->space->createUrl('/space/membership/revoke-membership'),
'data-action-confirm' => Yii::t('SpaceModule.base', 'Would you like to end your membership in Space {spaceName}?', ['{spaceName}' => '<strong>' . $this->space->getDisplayName() . '</strong>']),
'data-button-options' => Json::encode($this->options),
'data-ui-loader' => '',
'class' => 'btn btn-info active',
],
],
'cannotCancelMembership' => [
'visible' => false,
'memberTitle' => '<span class="glyphicon glyphicon-ok"></span>&nbsp;&nbsp;' . Yii::t('SpaceModule.base', 'Member'),
'ownerTitle' => '<span class="glyphicon glyphicon-user"></span>&nbsp;&nbsp;' . Yii::t('SpaceModule.base', 'Owner'),
'attrs' => ['class' => 'btn btn-info active'],
],
];
}
public function setDefaultOptions(array $defaultOptions)
{
$this->options = $this->getOptions($defaultOptions);
}
public function getOptions(array $defaultOptions = null): array
{
if ($defaultOptions === null) {
$defaultOptions = $this->getDefaultOptions();
}
return ArrayHelper::merge($defaultOptions, $this->options);
}
/**
* @inheritdoc
*/
public function run()
{
$membership = $this->space->getMembership();
return $this->render('membershipButton', [
'space' => $this->space,
'membership' => $membership
'space' => $this->space,
'membership' => $this->space->getMembership(),
'options' => $this->getOptions(),
'canCancelMembership' => !$this->space->isSpaceOwner() && $this->space->canLeave(),
]);
}

View File

@ -0,0 +1,60 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\space\widgets;
use humhub\components\Widget;
use humhub\modules\space\models\Space;
/**
* SpaceDirectoryActionButtons shows space directory buttons (following and membership)
*
* @since 1.9
* @author Luke
*/
class SpaceDirectoryActionButtons extends Widget
{
/**
* @var Space
*/
public $space;
/**
* @var string Template for buttons
*/
public $template = '{buttons}';
/**
* @inheritdoc
*/
public function run()
{
$html = FollowButton::widget([
'space' => $this->space,
]);
$html .= MembershipButton::widget([
'space' => $this->space,
'options' => [
'requestMembership' => ['attrs' => ['class' => 'btn btn-info btn-sm']],
'becomeMember' => ['attrs' => ['class' => 'btn btn-info btn-sm']],
'acceptInvite' => ['attrs' => ['class' => 'btn btn-info btn-sm'], 'togglerClass' => 'btn btn-info btn-sm'],
'cancelPendingMembership' => ['attrs' => ['class' => 'btn btn-info btn-sm active']],
'cancelMembership' => ['visible' => true, 'attrs' => ['class' => 'btn btn-info btn-sm active']],
'cannotCancelMembership' => ['visible' => true, 'attrs' => ['class' => 'btn btn-info btn-sm active']],
],
]);
if (trim($html) === '') {
return '';
}
return str_replace('{buttons}', $html, $this->template);
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\space\widgets;
use humhub\components\Widget;
use humhub\modules\space\models\Space;
/**
* SpaceDirectoryCard shows a space on spaces directory
*
* @since 1.9
* @author Luke
*/
class SpaceDirectoryCard extends Widget
{
/**
* @var Space
*/
public $space;
/**
* @var string HTML wrapper around card
*/
public $template = '<div class="card card-space col-lg-3 col-md-4 col-sm-6 col-xs-12">{card}</div>';
/**
* @inheritdoc
*/
public function run()
{
$card = $this->render('spaceDirectoryCard', [
'space' => $this->space
]);
return str_replace('{card}', $card, $this->template);
}
}

View File

@ -0,0 +1,71 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\space\widgets;
use humhub\libs\Html;
use humhub\modules\ui\widgets\DirectoryFilters;
use Yii;
/**
* SpaceDirectoryFilters displays the filters on the directory spaces page
*
* @since 1.9
* @author Luke
*/
class SpaceDirectoryFilters extends DirectoryFilters
{
/**
* @inheritdoc
*/
public $pageUrl = '/space/spaces';
protected function initDefaultFilters()
{
$this->addFilter('keyword', [
'title' => Yii::t('SpaceModule.base', 'Find Spaces by their description or by their Tags'),
'placeholder' => Yii::t('SpaceModule.base', 'Search...'),
'type' => 'input',
'wrapperClass' => 'col-md-6 form-search-filter-keyword',
'afterInput' => Html::submitButton('<span class="fa fa-search"></span>', ['class' => 'form-button-search']),
'sortOrder' => 100,
]);
$this->addFilter('sort', [
'title' => Yii::t('SpaceModule.base', 'Sorting'),
'type' => 'dropdown',
'options' => [
'name' => Yii::t('SpaceModule.base', 'By Name'),
'newer' => Yii::t('SpaceModule.base', 'Newest first'),
'older' => Yii::t('SpaceModule.base', 'Oldest first'),
],
'sortOrder' => 200,
]);
$this->addFilter('connection', [
'title' => Yii::t('SpaceModule.base', 'Status'),
'type' => 'dropdown',
'options' => [
'' => Yii::t('SpaceModule.base', 'All'),
'member' => Yii::t('SpaceModule.base', 'Member'),
'follow' => Yii::t('SpaceModule.base', 'Following'),
'none' => Yii::t('SpaceModule.base', 'Neither member nor following'),
],
'sortOrder' => 300,
]);
}
public static function getDefaultValue(string $filter): string
{
switch ($filter) {
case 'sort':
return 'name';
}
return parent::getDefaultValue($filter);
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\space\widgets;
use humhub\components\Widget;
use humhub\modules\space\models\Space;
/**
* SpaceDirectoryIcons shows footer icons for spaces cards
*
* @since 1.9
* @author Luke
*/
class SpaceDirectoryIcons extends Widget
{
/**
* @var Space
*/
public $space;
/**
* @inheritdoc
*/
public function run()
{
return $this->render('spaceDirectoryIcons', [
'space' => $this->space
]);
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\space\widgets;
use humhub\libs\Html;
use yii\helpers\Url;
use humhub\components\Widget;
/**
* SpaceDirectoryTagList displays the space tags on the directory spaces page
*
* @since 1.9
* @author Luke
*/
class SpaceDirectoryTagList extends Widget
{
/**
* @var \humhub\modules\space\models\Space
*/
public $space;
/**
* @var int number of max. displayed tags
*/
public $maxTags = 5;
/**
* @var string Template for tags
*/
public $template = '{tags}';
/**
* @inheritdoc
*/
public function run()
{
$html = '';
$tags = $this->space->getTags();
$count = count($tags);
if ($count === 0) {
return $html;
} elseif ($count > $this->maxTags) {
$tags = array_slice($tags, 0, $this->maxTags);
}
$html = '';
foreach ($tags as $tag) {
if (trim($tag) !== '') {
$html .= Html::a(Html::encode($tag), Url::to(['/space/spaces', 'keyword' => trim($tag)]), ['class' => 'label label-default']) . "&nbsp";
}
}
if ($html === '') {
return $html;
}
return str_replace('{tags}', $html, $this->template);
}
}

View File

@ -4,27 +4,39 @@ use humhub\modules\space\models\Space;
use humhub\modules\space\models\Membership;
use yii\helpers\Html;
/* @var $membership Membership */
/* @var $space Space */
/* @var $options array */
/* @var $canCancelMembership bool */
if ($membership === null) {
if ($space->canJoin()) {
if ($space->join_policy == Space::JOIN_POLICY_APPLICATION) {
echo Html::a(Yii::t('SpaceModule.base', 'Request membership'), $space->createUrl('/space/membership/request-membership-form'), ['id' => 'requestMembershipButton', 'class' => 'btn btn-primary', 'data-target' => '#globalModal']);
echo Html::a($options['requestMembership']['title'], $options['requestMembership']['url'], $options['requestMembership']['attrs']);
} else {
echo Html::a(Yii::t('SpaceModule.base', 'Become member'), $space->createUrl('/space/membership/request-membership'), ['id' => 'requestMembershipButton', 'class' => 'btn btn-primary', 'data-method' => 'POST']);
echo Html::a($options['becomeMember']['title'], '#', $options['becomeMember']['attrs']);
}
}
} elseif ($membership->status == Membership::STATUS_INVITED) {
?>
<div class="btn-group">
<?= Html::a(Yii::t('SpaceModule.base', 'Accept Invite'), $space->createUrl('/space/membership/invite-accept'), ['class' => 'btn btn-info', 'data-method' => 'POST']); ?>
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<div class="<?= $options['acceptInvite']['groupClass'] ?>">
<?= Html::a($options['acceptInvite']['title'], '#', $options['acceptInvite']['attrs']); ?>
<button type="button" class="<?= $options['acceptInvite']['togglerClass'] ?> dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><?= Html::a(Yii::t('SpaceModule.base', 'Decline Invite'), $space->createUrl('/space/membership/revoke-membership'), ['data-method' => 'POST']); ?></li>
<li><?= Html::a($options['declineInvite']['title'], '#', $options['declineInvite']['attrs']); ?></li>
</ul>
</div>
<?php
} elseif ($membership->status == Membership::STATUS_APPLICANT) {
echo Html::a(Yii::t('SpaceModule.base', 'Cancel pending membership application'), $space->createUrl('/space/membership/revoke-membership'), ['data-method' => 'POST', 'class' => 'btn btn-primary']);
echo Html::a($options['cancelPendingMembership']['title'], $space->createUrl('/space/membership/revoke-membership'), $options['cancelPendingMembership']['attrs']);
} elseif ($membership->status == Membership::STATUS_MEMBER) {
if ($canCancelMembership && $options['cancelMembership']['visible']) {
echo Html::a($options['cancelMembership']['title'], '#', $options['cancelMembership']['attrs']);
} elseif (!$canCancelMembership && $options['cannotCancelMembership']['visible']) {
$memberTitle = (!$space->isSpaceOwner() ? $options['cannotCancelMembership']['ownerTitle'] : $options['cannotCancelMembership']['memberTitle']);
echo Html::a($memberTitle, $space->createUrl(), $options['cannotCancelMembership']['attrs']);
}
}

View File

@ -25,7 +25,7 @@ use humhub\modules\space\widgets\MembershipButton;
[FollowButton::class, [
'space' => $container,
'followOptions' => ['class' => 'btn btn-primary'],
'unfollowOptions' => ['class' => 'btn btn-info']
'unfollowOptions' => ['class' => 'btn btn-primary active']
], ['sortOrder' => 30]]
]]); ?>
<?= HeaderControlsMenu::widget(['space' => $container]); ?>

View File

@ -0,0 +1,47 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\libs\Html;
use humhub\modules\space\models\Space;
use humhub\modules\space\widgets\Image;
use humhub\modules\space\widgets\SpaceDirectoryActionButtons;
use humhub\modules\space\widgets\SpaceDirectoryIcons;
use humhub\modules\space\widgets\SpaceDirectoryTagList;
use yii\web\View;
/* @var $this View */
/* @var $space Space */
?>
<div class="card-panel">
<div class="card-bg-image"<?php if ($space->getProfileBannerImage()->hasImage()) : ?> style="background-image: url('<?= $space->getProfileBannerImage()->getUrl() ?>')"<?php endif; ?>></div>
<div class="card-header">
<?= Image::widget([
'space' => $space,
'link' => true,
'linkOptions' => ['data-contentcontainer-id' => $space->contentcontainer_id, 'class' => 'card-image-link'],
'width' => 94,
]); ?>
<div class="card-icons">
<?= SpaceDirectoryIcons::widget(['space' => $space]); ?>
</div>
</div>
<div class="card-body">
<strong class="card-title"><?= Html::containerLink($space); ?></strong>
<?php if (trim($space->description) !== '') : ?>
<div class="card-details"><?= Html::encode($space->description); ?></div>
<?php endif; ?>
<?= SpaceDirectoryTagList::widget([
'space' => $space,
'template' => '<div class="card-tags">{tags}</div>',
]); ?>
</div>
<?= SpaceDirectoryActionButtons::widget([
'space' => $space,
'template' => '<div class="card-footer">{buttons}</div>',
]); ?>
</div>

View File

@ -0,0 +1,16 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\modules\space\models\Space;
use yii\helpers\Url;
use yii\web\View;
/* @var $this View */
/* @var $space Space */
?>
<a href="#" class="fa fa-users" data-action-click="ui.modal.load" data-action-url="<?= Url::to(['/space/membership/members-list', 'container' => $space]) ?>"> <span><?= $space->getMemberships()->count() ?></span></a>

View File

@ -0,0 +1,124 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\ui\widgets;
use humhub\components\Widget;
use humhub\libs\Html;
use Yii;
use yii\helpers\ArrayHelper;
/**
* DirectoryFilters displays the filters on the directory people/spaces pages
*
* @since 1.9
* @author Luke
*/
abstract class DirectoryFilters extends Widget
{
/**
* @var array Filters
*/
protected $filters = [];
/**
* @var string Main page URL, used to reset and submit a form with filters
*/
public $pageUrl;
public function init()
{
$this->initDefaultFilters();
parent::init();
ArrayHelper::multisort($this->filters, 'sortOrder');
}
abstract protected function initDefaultFilters();
/**
* @inheritdoc
*/
public function run()
{
return $this->render('@humhub/modules/ui/widgets/views/directoryFilters', ['directoryFilters' => $this]);
}
public function renderFilters(): string
{
$filtersHtml = '';
foreach ($this->filters as $filter => $data) {
$filtersHtml .= $this->render('@humhub/modules/ui/widgets/views/directoryFilter', [
'filter' => $filter,
'data' => array_merge(self::getDefaultFilterData(), $data),
]);
}
return $filtersHtml;
}
public static function getDefaultFilterData(): array
{
return [
'wrapperClass' => 'col-md-2',
'titleClass' => 'form-search-field-info',
'inputClass' => 'form-control form-search-filter',
'beforeInput' => '',
'afterInput' => '',
];
}
public static function renderFilterInput(string $filter, array $data): string
{
$inputOptions = ['class' => $data['inputClass']];
if (isset($data['inputOptions'])) {
$inputOptions = array_merge($inputOptions, $data['inputOptions']);
}
switch ($data['type']) {
case 'dropdown':
case 'dropdownlist':
$inputOptions['data-action-change'] = 'directory.applyFilters';
$inputHtml = Html::dropDownList($filter, self::getValue($filter), $data['options'], $inputOptions);
break;
case 'input':
case 'text':
default:
if (isset($data['placeholder'])) {
$inputOptions['placeholder'] = $data['placeholder'];
}
$inputHtml = Html::textInput($filter, self::getValue($filter), $inputOptions);
}
return $data['beforeInput'].$inputHtml.$data['afterInput'];
}
public function addFilter(string $filterKey, array $filterData)
{
$this->filters[$filterKey] = $filterData;
}
public static function getDefaultValue(string $filter): string
{
return '';
}
public static function getValue(string $filter)
{
$defaultValue = self::getDefaultValue($filter);
if (preg_match('/^(.+?)\[(.+?)\]$/', $filter, $arrayMatch)) {
$array = Yii::$app->request->get($arrayMatch[1]);
return isset($array[$arrayMatch[2]]) ? $array[$arrayMatch[2]] : $defaultValue;
}
return Yii::$app->request->get($filter, $defaultValue);
}
}

View File

@ -0,0 +1,17 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\modules\ui\widgets\DirectoryFilters;
/* @var $filter string */
/* @var $data array */
?>
<div class="<?= $data['wrapperClass'] ?>">
<div class="<?= $data['titleClass'] ?>"><?= $data['title'] ?></div>
<?= DirectoryFilters::renderFilterInput($filter, $data) ?>
</div>

View File

@ -0,0 +1,23 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\libs\Html;
use humhub\modules\ui\widgets\DirectoryFilters;
use yii\helpers\Url;
/* @var $directoryFilters DirectoryFilters */
?>
<?= Html::beginForm(Url::to([$directoryFilters->pageUrl]), 'get', ['class' => 'form-search']); ?>
<?= Html::hiddenInput('page', '1'); ?>
<div class="row">
<?= $directoryFilters->renderFilters() ?>
<div class="col-md-2 form-search-without-info">
<?= Html::a(Yii::t('UiModule.base', 'Reset filters'), Url::to([$directoryFilters->pageUrl]), ['class' => 'form-search-reset']); ?>
</div>
</div>
<?= Html::endForm(); ?>

View File

@ -2,13 +2,16 @@
namespace humhub\modules\user;
use humhub\components\Event;
use humhub\modules\content\models\ContentContainer;
use humhub\modules\ui\menu\MenuLink;
use humhub\modules\user\models\User;
use humhub\modules\user\models\Password;
use humhub\modules\user\models\Profile;
use humhub\modules\user\models\GroupUser;
use humhub\modules\user\models\Mentioning;
use humhub\modules\user\models\Follow;
use humhub\modules\user\permissions\PeopleAccess;
use Yii;
use yii\base\BaseObject;
@ -154,4 +157,29 @@ class Events extends BaseObject
Yii::$app->queue->push(new jobs\DeleteExpiredSessions());
}
/**
* On build of the TopMenu
*
* @param Event $event
*/
public static function onTopMenuInit($event)
{
if (Yii::$app->user->isGuest) {
return;
}
if (!Yii::$app->user->can(PeopleAccess::class)) {
return;
}
$event->sender->addEntry(new MenuLink([
'id' => 'people',
'icon' => 'users',
'label' => Yii::t('UserModule.base', 'People'),
'url' => ['/user/people'],
'sortOrder' => 200,
'isActive' => MenuLink::isActiveState('user', 'people'),
]));
}
}

View File

@ -8,6 +8,7 @@
namespace humhub\modules\user;
use humhub\modules\space\models\Space;
use humhub\modules\user\models\Group;
use Yii;
@ -146,9 +147,13 @@ class Module extends \humhub\components\Module
return [
new permissions\ViewAboutPage(),
];
} elseif ($contentContainer instanceof Space) {
return [];
}
return [];
return [
new permissions\PeopleAccess(),
];
}
/**

View File

@ -0,0 +1,225 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\components;
use humhub\modules\user\models\fieldtype\Select;
use humhub\modules\user\models\Group;
use humhub\modules\user\models\ProfileField;
use humhub\modules\user\models\User;
use humhub\modules\user\widgets\PeopleFilters;
use Yii;
use yii\data\Pagination;
use yii\db\Expression;
/**
* PeopleQuery is used to query User records on the People page.
*
* @author luke
*/
class PeopleQuery extends ActiveQueryUser
{
/**
* @var Group
*/
public $filteredGroup;
/**
* @var Pagination
*/
public $pagination;
/**
* @var int
*/
public $pageSize = 18;
/**
* @inheritdoc
*/
public function __construct($config = [])
{
parent::__construct(User::class, $config);
}
/**
* @inheritdoc
*/
public function init()
{
parent::init();
$this->visible();
$this->filterByKeyword();
$this->filterByGroup();
$this->filterByConnection();
$this->filterByProfileFields();
$this->order();
$this->paginate();
}
public function filterByKeyword(): PeopleQuery
{
$keyword = Yii::$app->request->get('keyword', '');
return $this->search($keyword);
}
public function filterByProfileFields(): PeopleQuery
{
$fields = Yii::$app->request->get('fields', []);
// Remove empty filters
$fields = array_filter($fields, function($value) {
return $value !== '';
});
if (empty($fields)) {
return $this;
}
// Skip fields if they are not defined for directory filters
$filteredProfileFields = ProfileField::find()
->where(['directory_filter' => 1])
->andWhere(['IN', 'internal_name', array_keys($fields)])
->all();
$checkedFilteredFields = [];
foreach ($filteredProfileFields as $filteredField) {
/* @var $filteredField ProfileField */
if (!isset($fields[$filteredField->internal_name])) {
// Skip unknown field
continue;
}
$checkedFilteredFields[$filteredField->internal_name] = [
'value' => $fields[$filteredField->internal_name],
'condition' => $filteredField->getFieldType() instanceof Select ? '=' : 'LIKE',
];
}
if (empty($checkedFilteredFields)) {
return $this;
}
$this->joinWith('profile');
foreach ($checkedFilteredFields as $field => $data) {
$this->andWhere([$data['condition'], 'profile.' . $field, $data['value']]);
}
return $this;
}
public function filterByGroup(): PeopleQuery
{
$groupId = Yii::$app->request->get('groupId', 0);
if ($groupId) {
$group = Group::findOne(['id' => $groupId, 'show_at_directory' => 1]);
if ($group) {
$this->filteredGroup = $group;
$this->isGroupMember($group);
}
}
return $this;
}
public function filterByConnection(): PeopleQuery
{
switch (Yii::$app->request->get('connection')) {
case 'followers':
return $this->filterByConnectionFollowers();
case 'following':
return $this->filterByConnectionFollowing();
case 'friends':
return $this->filterByConnectionFriends();
case 'pending_friends':
return $this->filterByConnectionPendingFriends();
}
return $this;
}
public function filterByConnectionFollowers(): PeopleQuery
{
return $this->innerJoin('user_follow', 'user_follow.object_model = :user_class AND user_follow.user_id = user.id', [':user_class' => User::class])
->andWhere(['user_follow.object_id' => Yii::$app->user->id]);
}
public function filterByConnectionFollowing(): PeopleQuery
{
return $this->innerJoin('user_follow', 'user_follow.object_model = :user_class AND user_follow.object_id = user.id', [':user_class' => User::class])
->andWhere(['user_follow.user_id' => Yii::$app->user->id]);
}
public function filterByConnectionFriends(): PeopleQuery
{
if (!Yii::$app->getModule('friendship')->settings->get('enable')) {
return $this;
}
return $this->innerJoin('user_friendship AS uf_current', 'uf_current.friend_user_id = user.id')
->andWhere(['uf_current.user_id' => Yii::$app->user->id])
->innerJoin('user_friendship AS uf_friend', 'uf_friend.user_id = user.id')
->andWhere(['uf_friend.friend_user_id' => Yii::$app->user->id]);
}
public function filterByConnectionPendingFriends(): PeopleQuery
{
if (!Yii::$app->getModule('friendship')->settings->get('enable')) {
return $this;
}
return $this->innerJoin('user_friendship AS uf_current', 'uf_current.friend_user_id = user.id')
->andWhere(['uf_current.user_id' => Yii::$app->user->id])
->leftJoin('user_friendship AS uf_friend', 'uf_friend.user_id = user.id')
->andWhere(['IS', 'uf_friend.friend_user_id', new Expression('NULL')]);
}
public function isFilteredByGroup(): bool
{
return $this->filteredGroup instanceof Group;
}
public function order(): PeopleQuery
{
switch (PeopleFilters::getValue('sort')) {
case 'firstname':
$this->joinWith('profile');
$this->addOrderBy('profile.firstname');
break;
case 'lastname':
$this->joinWith('profile');
$this->addOrderBy('profile.lastname');
break;
case 'lastlogin':
$this->addOrderBy('last_login DESC');
break;
}
return $this;
}
public function paginate(): PeopleQuery
{
$countQuery = clone $this;
$this->pagination = new Pagination(['totalCount' => $countQuery->count(), 'pageSize' => $this->pageSize]);
return $this->offset($this->pagination->offset)->limit($this->pagination->limit);
}
public function isLastPage(): bool
{
return $this->pagination->getPage() == $this->pagination->getPageCount() - 1;
}
}

View File

@ -6,13 +6,15 @@ use humhub\commands\IntegrityController;
use humhub\modules\content\components\ContentAddonActiveRecord;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\commands\CronController;
use humhub\widgets\TopMenu;
return [
'id' => 'user',
'class' => \humhub\modules\user\Module::class,
'isCoreModule' => true,
'urlManagerRules' => [
['class' => 'humhub\modules\user\components\UrlRule']
['class' => 'humhub\modules\user\components\UrlRule'],
'people' => 'user/people',
],
'consoleControllerMap' => [
'user' => 'humhub\modules\user\commands\UserController'
@ -23,6 +25,7 @@ return [
['class' => ContentAddonActiveRecord::class, 'event' => ContentAddonActiveRecord::EVENT_BEFORE_DELETE, 'callback' => [Events::class, 'onContentDelete']],
['class' => IntegrityController::class, 'event' => IntegrityController::EVENT_ON_RUN, 'callback' => [Events::class, 'onIntegrityCheck']],
['class' => CronController::class, 'event' => CronController::EVENT_ON_HOURLY_RUN, 'callback' => [Events::class, 'onHourlyCron']],
['class' => TopMenu::class, 'event' => TopMenu::EVENT_INIT, 'callback' => [Events::class, 'onTopMenuInit']],
]
];
?>

View File

@ -0,0 +1,88 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\controllers;
use humhub\components\access\ControllerAccess;
use humhub\components\Controller;
use humhub\modules\user\components\PeopleQuery;
use humhub\modules\user\permissions\PeopleAccess;
use humhub\modules\user\widgets\PeopleCard;use Yii;
use yii\helpers\Url;
/**
* PeopleController displays users directory
*
* @since 1.9
*/
class PeopleController extends Controller
{
/**
* @inheritdoc
*/
public $subLayout = '@user/views/people/_layout';
/**
* @inheritdoc
*/
public function init()
{
$this->setActionTitles([
'index' => Yii::t('UserModule.base', 'People'),
]);
parent::init();
}
/**
* @inheritdoc
*/
public function getAccessRules()
{
return [
[ControllerAccess::RULE_LOGGED_IN_ONLY],
['permissions' => [PeopleAccess::class]],
];
}
/**
* Action to display people page
*/
public function actionIndex()
{
$peopleQuery = new PeopleQuery();
$urlParams = Yii::$app->request->getQueryParams();
unset($urlParams['page']);
array_unshift($urlParams, '/user/people/load-more');
$this->getView()->registerJsConfig('directory', [
'loadMoreUrl' => Url::to($urlParams),
]);
return $this->render('index', [
'people' => $peopleQuery,
'showInviteButton' => !Yii::$app->user->isGuest && Yii::$app->getModule('user')->settings->get('auth.internalUsersCanInvite'),
]);
}
/**
* Action to load cards for next page by AJAX
*/
public function actionLoadMore()
{
$peopleQuery = new PeopleQuery();
$peopleCards = '';
foreach ($peopleQuery->all() as $user) {
$peopleCards .= PeopleCard::widget(['user' => $user]);
}
return $peopleCards;
}
}

View File

@ -0,0 +1,27 @@
<?php
use yii\db\Migration;
/**
* Class m210506_060737_profile_field_directory_filter
*/
class m210506_060737_profile_field_directory_filter extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->addColumn('profile_field', 'directory_filter', $this->tinyInteger(1)->notNull()->defaultValue(0));
$this->createIndex('index_directory_filter', 'profile_field', 'directory_filter');
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropColumn('profile_field', 'directory_filter');
}
}

View File

@ -11,6 +11,8 @@ namespace humhub\modules\user\models;
use humhub\components\ActiveRecord;
use humhub\libs\Helpers;
use humhub\modules\user\models\fieldtype\BaseType;
use humhub\modules\user\models\fieldtype\Select;
use humhub\modules\user\models\fieldtype\Text;
use Yii;
use yii\db\ActiveQuery;
@ -38,6 +40,7 @@ use yii\db\ActiveQuery;
* @property string $translation_category
* @property integer $is_system
* @property integer $searchable
* @property integer $directory_filter
*/
class ProfileField extends ActiveRecord
{
@ -64,7 +67,7 @@ class ProfileField extends ActiveRecord
{
return [
[['profile_field_category_id', 'field_type_class', 'internal_name', 'title', 'sort_order'], 'required'],
[['profile_field_category_id', 'required', 'editable', 'searchable', 'show_at_registration', 'visible', 'sort_order'], 'integer'],
[['profile_field_category_id', 'required', 'editable', 'searchable', 'show_at_registration', 'visible', 'sort_order', 'directory_filter'], 'integer'],
[['module_id', 'field_type_class', 'title'], 'string', 'max' => 255],
['internal_name', 'string', 'max' => 100],
[['ldap_attribute', 'translation_category'], 'string', 'max' => 255],
@ -103,6 +106,7 @@ class ProfileField extends ActiveRecord
'translation_category' => Yii::t('UserModule.profile', 'Translation Category ID'),
'required' => Yii::t('UserModule.profile', 'Required'),
'searchable' => Yii::t('UserModule.profile', 'Searchable'),
'directory_filter' => Yii::t('UserModule.profile', 'Use as Directory filter'),
'title' => Yii::t('UserModule.profile', 'Title'),
'description' => Yii::t('UserModule.profile', 'Description'),
'sort_order' => Yii::t('UserModule.profile', 'Sort order'),
@ -169,6 +173,7 @@ class ProfileField extends ActiveRecord
$categories = ProfileFieldCategory::find()->orderBy('sort_order')->all();
$profileFieldTypes = new fieldtype\BaseType();
$isVirtualField = (!$this->isNewRecord && $this->getFieldType()->isVirtual);
$canBeDirectoryFilter = (!$this->isNewRecord && $this->getFieldType()->canBeDirectoryFilter);
return [
'ProfileField' => [
@ -227,6 +232,10 @@ class ProfileField extends ActiveRecord
'type' => 'checkbox',
'isVisible' => (!$isVirtualField)
],
'directory_filter' => [
'type' => 'checkbox',
'isVisible' => ($canBeDirectoryFilter)
],
'profile_field_category_id' => [
'type' => 'dropdownlist',
'items' => \yii\helpers\ArrayHelper::map($categories, 'id', 'title'),

View File

@ -46,7 +46,6 @@ class BaseType extends Model
*/
public $profileField = null;
/**
* @var boolean is a virtual field (readonly)
* @see BaseTypeVirtual
@ -54,6 +53,12 @@ class BaseType extends Model
*/
public $isVirtual = false;
/**
* @var boolean can be used as directory filter (readonly)
* @since 1.9
*/
public $canBeDirectoryFilter = false;
/**
* Links a ProfileField to the ProfileFieldType.
*

View File

@ -64,7 +64,7 @@ abstract class BaseTypeVirtual extends BaseType
*/
protected static function getHiddenFormFields()
{
return ['searchable', 'required', 'show_at_registration', 'editable'];
return ['searchable', 'required', 'show_at_registration', 'editable', 'directory_filter'];
}
/**
@ -85,6 +85,7 @@ abstract class BaseTypeVirtual extends BaseType
$this->profileField->searchable = 0;
$this->profileField->required = 0;
$this->profileField->show_at_registration = 0;
$this->profileField->directory_filter = 0;
return parent::save();
}

View File

@ -29,6 +29,11 @@ class Select extends BaseType
*/
public $options;
/**
* @inerhitdoc
*/
public $canBeDirectoryFilter = true;
/**
* Rules for validating the Field Type Settings Form
*

View File

@ -64,6 +64,11 @@ class Text extends BaseType
*/
public $regexpErrorMessage;
/**
* @inerhitdoc
*/
public $canBeDirectoryFilter = true;
/**
* Rules for validating the Field Type Settings Form
*

View File

@ -0,0 +1,40 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\permissions;
use humhub\libs\BasePermission;
use Yii;
class PeopleAccess extends BasePermission
{
/**
* @inheritdoc
*/
protected $moduleId = 'user';
/**
* @inheritdoc
*/
protected $defaultState = self::STATE_ALLOW;
/**
* @inheritdoc
*/
public function getTitle()
{
return Yii::t('UserModule.permissions', 'Can Access People');
}
/**
* @inheritdoc
*/
public function getDescription()
{
return Yii::t('UserModule.permissions', 'Can access the people section.');
}
}

View File

@ -8,7 +8,6 @@
namespace user\functional;
use humhub\modules\user\models\User;
use user\FunctionalTester;
use Yii;
use yii\helpers\Url;
@ -23,7 +22,7 @@ class MailInviteCest
$I->amUser2();
$I->amOnDirectory()->clickMembers();
$I->amGoingTo('invte a user by mail');
$I->amGoingTo('invite a user by mail');
$I->see('Send invite', 'button');
@ -49,11 +48,11 @@ class MailInviteCest
$I->amOnRoute('/user/registration', ['token' => $token]);
$I->see('Account registration');
$I->fillField( 'User[username]', 'NewUser');
$I->fillField( 'Password[newPassword]', 'NewUser123');
$I->fillField( 'Password[newPasswordConfirm]', 'NewUser123');
$I->fillField( 'Profile[firstname]', 'New');
$I->fillField( 'Profile[lastname]', 'User');
$I->fillField('User[username]', 'NewUser');
$I->fillField('Password[newPassword]', 'NewUser123');
$I->fillField('Password[newPasswordConfirm]', 'NewUser123');
$I->fillField('Profile[firstname]', 'New');
$I->fillField('Profile[lastname]', 'User');
$I->click('#registration-form [type="submit"]');
@ -91,14 +90,13 @@ class MailInviteCest
$I->amOnRoute('/user/registration', ['token' => $token]);
$I->see('Account registration');
$I->fillField( 'User[username]', 'NewUser');
$I->fillField( 'Password[newPassword]', 'NewUser123');
$I->fillField( 'Password[newPasswordConfirm]', 'NewUser123');
$I->fillField( 'Profile[firstname]', 'New');
$I->fillField( 'Profile[lastname]', 'User');
$I->fillField('User[username]', 'NewUser');
$I->fillField('Password[newPassword]', 'NewUser123');
$I->fillField('Password[newPasswordConfirm]', 'NewUser123');
$I->fillField('Profile[firstname]', 'New');
$I->fillField('Profile[lastname]', 'User');
$I->click('#registration-form [type="submit"]');
$I->see('Dashboard');
}
}

View File

@ -0,0 +1,9 @@
<?php
use humhub\modules\ui\view\helpers\ThemeHelper;
/* @var $content string */
?>
<div class="<?php if (ThemeHelper::isFluid()): ?>container-fluid<?php else: ?>container<?php endif; ?> container-directory container-people">
<?= $content; ?>
</div>

View File

@ -0,0 +1,63 @@
<?php
use humhub\assets\DirectoryAsset;
use humhub\libs\Html;
use humhub\modules\user\components\PeopleQuery;
use humhub\modules\user\widgets\PeopleCard;
use humhub\modules\user\widgets\PeopleFilters;
use humhub\widgets\Button;
use humhub\widgets\ModalButton;
/* @var $this \yii\web\View */
/* @var $people PeopleQuery */
/* @var $showInviteButton bool */
DirectoryAsset::register($this);
?>
<div class="panel panel-default">
<div class="panel-heading">
<?php if ($people->isFilteredByGroup()) : ?>
<?= Yii::t('UserModule.base', '<strong>Group</strong> members - {group}', ['{group}' => Html::encode($people->filteredGroup->name)]); ?>
<?php else: ?>
<?= Yii::t('UserModule.base', '<strong>People</strong>'); ?>
<?php endif; ?>
<?php if ($showInviteButton): ?>
<?= ModalButton::primary(Yii::t('UserModule.base', 'Send invite'))
->load(['/user/invite'])->icon('invite')->sm()->right() ?>
<?php endif; ?>
</div>
<div class="panel-body">
<?= PeopleFilters::widget(); ?>
</div>
</div>
<div class="row cards">
<?php if (!$people->exists()): ?>
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-body">
<?= Yii::t('UserModule.base', 'No results found! Try other keywords or remove filters.'); ?>
</div>
</div>
</div>
<?php endif; ?>
<?php foreach ($people->all() as $user) : ?>
<?= PeopleCard::widget(['user' => $user]); ?>
<?php endforeach; ?>
</div>
<?php if (!$people->isLastPage()) : ?>
<div class="directory-load-more">
<?= Button::primary(Yii::t('UserModule.base', 'Load more'))
->icon('fa-angle-down')
->action('directory.loadMore')
->options([
'data-current-page' => $people->pagination->getPage() + 1,
'data-total-pages' => $people->pagination->getPageCount(),
]) ?>
</div>
<?php endif; ?>

View File

@ -0,0 +1,61 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\widgets;
use humhub\components\Widget;
use humhub\modules\friendship\widgets\FriendshipButton;
use humhub\modules\user\models\User;
/**
* PeopleActionsButton shows directory options (following or friendship) for listed users
*
* @since 1.9
* @author Luke
*/
class PeopleActionButtons extends Widget
{
/**
* @var User
*/
public $user;
/**
* @var string Template for buttons
*/
public $template = '{buttons}';
/**
* @inheritdoc
*/
public function run()
{
$html = UserFollowButton::widget([
'user' => $this->user,
'followOptions' => ['class' => 'btn btn-primary btn-sm'],
'unfollowOptions' => ['class' => 'btn btn-primary btn-sm active'],
]);
$html .= FriendshipButton::widget([
'user' => $this->user,
'options' => [
'friends' => ['attrs' => ['class' => 'btn btn-info btn-sm active']],
'addFriend' => ['attrs' => ['class' => 'btn btn-info btn-sm']],
'acceptFriendRequest' => ['attrs' => ['class' => 'btn btn-info btn-sm active'], 'togglerClass' => 'btn btn-info btn-sm active'],
'cancelFriendRequest' => ['attrs' => ['class' => 'btn btn-info btn-sm active']],
],
]);
if (trim($html) === '') {
return '';
}
return str_replace('{buttons}', $html, $this->template);
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\widgets;
use humhub\components\Widget;
use humhub\modules\admin\models\forms\PeopleSettingsForm;
use humhub\modules\user\models\User;
/**
* PeopleActionsButton shows directory options (following or friendship) for listed users
*
* @since 1.9
* @author Luke
*/
class PeopleCard extends Widget
{
/**
* @var User
*/
public $user;
/**
* @var string HTML wrapper around card
*/
public $template = '<div class="card card-people col-lg-3 col-md-4 col-sm-6 col-xs-12">{card}</div>';
/**
* @inheritdoc
*/
public function run()
{
$card = $this->render('peopleCard', [
'user' => $this->user
]);
return str_replace('{card}', $card, $this->template);
}
public static function config($name): string
{
$peopleSettingsForm = new PeopleSettingsForm();
return isset($peopleSettingsForm->$name) ? $peopleSettingsForm->$name : '';
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\widgets;
use humhub\components\Widget;
use humhub\modules\user\models\ProfileField;
use humhub\modules\user\models\User;
/**
* PeopleDetails shows details for back side of the people card
*
* @since 1.9
* @author Luke
*/
class PeopleDetails extends Widget
{
/**
* @var User
*/
public $user;
/**
* @var string Separator between lines
*/
public $separator = '<br>';
/**
* @var string Template for lines
*/
public $template = '{lines}';
/**
* @inheritdoc
*/
public function run()
{
$lines = [];
for ($i = 1; $i <= 3; $i++) {
if ($profileField = $this->getProfileFieldValue(PeopleCard::config('detail' . $i))) {
$lines[] = $profileField;
}
}
if (empty($lines)) {
return '';
}
return str_replace('{lines}', implode($this->separator, $lines), $this->template);
}
/**
* Get user profile field value by internal name
*
* @param string $internalName
* @return false|string
*/
private function getProfileFieldValue(string $internalName)
{
if (empty($internalName)) {
return false;
}
$profileField = ProfileField::find()
->where(['visible' => 1])
->andWhere(['internal_name' => $internalName])
->one();
if (!$profileField) {
return false;
}
return $profileField->getUserValue($this->user, false);
}
}

View File

@ -0,0 +1,136 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\widgets;
use humhub\libs\Html;
use humhub\modules\admin\models\forms\PeopleSettingsForm;
use humhub\modules\ui\widgets\DirectoryFilters;
use humhub\modules\user\models\Group;
use humhub\modules\user\models\ProfileField;
use Yii;
/**
* PeopleFilters displays the filters on the directory people page
*
* @since 1.9
* @author Luke
*/
class PeopleFilters extends DirectoryFilters
{
/**
* @inheritdoc
*/
public $pageUrl = '/user/people';
protected function initDefaultFilters()
{
// Keyword
$this->addFilter('keyword', [
'title' => Yii::t('UserModule.base', 'Find people by their profile data or user tags'),
'placeholder' => Yii::t('UserModule.base', 'Search...'),
'type' => 'input',
'wrapperClass' => 'col-md-6 form-search-filter-keyword',
'afterInput' => Html::submitButton('<span class="fa fa-search"></span>', ['class' => 'form-button-search']),
'sortOrder' => 100,
]);
// Group
$groupOptions = [];
$groups = Group::findAll(['show_at_directory' => 1]);
if ($groups) {
$groupOptions[''] = Yii::t('UserModule.base', 'Any');
foreach ($groups as $group) {
$groupOptions[$group->id] = $group->name;
}
$this->addFilter('groupId', [
'title' => Yii::t('UserModule.base', 'User Group'),
'type' => 'dropdown',
'options' => $groupOptions,
'sortOrder' => 200,
]);
}
// Sorting
$this->addFilter('sort', [
'title' => Yii::t('SpaceModule.base', 'Sorting'),
'type' => 'dropdown',
'options' => PeopleSettingsForm::getSortingOptions(),
'sortOrder' => 300,
]);
// Connection
$connectionOptions = [
'' => Yii::t('UserModule.base', 'All'),
'followers' => Yii::t('UserModule.base', 'Followers'),
'following' => Yii::t('UserModule.base', 'Following'),
];
if (Yii::$app->getModule('friendship')->settings->get('enable')) {
$connectionOptions['friends'] = Yii::t('UserModule.base', 'Friends');
$connectionOptions['pending_friends'] = Yii::t('UserModule.base', 'Pending Requests');
}
$this->addFilter('connection', [
'title' => Yii::t('SpaceModule.base', 'Status'),
'type' => 'dropdown',
'options' => $connectionOptions,
'sortOrder' => 400,
]);
// Profile fields
$profileFields = ProfileField::findAll(['directory_filter' => 1]);
$profileFieldSortOrder = 1000;
foreach ($profileFields as $profileField) {
$this->initProfileFieldFilter($profileField, $profileFieldSortOrder);
$profileFieldSortOrder += 100;
}
}
private function initProfileFieldFilter(ProfileField $profileField, $sortOrder = 1000)
{
$profileFieldType = $profileField->getFieldType();
if (!$profileFieldType) {
return;
}
$definition = $profileFieldType->getFieldFormDefinition();
$fieldType = isset($definition[$profileField->internal_name]['type']) ? $definition[$profileField->internal_name]['type'] : null;
$filterData = [
'title' => Yii::t($profileField->getTranslationCategory(), $profileField->title),
'type' => $fieldType,
'sortOrder' => $sortOrder,
];
switch ($fieldType) {
case 'text':
break;
case 'dropdownlist':
$filterData['options'] = array_merge(['' => Yii::t('UserModule.base', 'Any')], $definition[$profileField->internal_name]['items']);
break;
default:
// Skip not supported type
return;
}
$this->addFilter('fields[' . $profileField->internal_name . ']', $filterData);
}
public static function getDefaultValue(string $filter): string
{
switch ($filter) {
case 'sort':
return PeopleCard::config('defaultSorting');
}
return '';
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\widgets;
use humhub\components\Widget;
use humhub\modules\user\models\User;
/**
* PeopleIcons shows footer icons for people cards
*
* @since 1.9
* @author Luke
*/
class PeopleIcons extends Widget
{
/**
* @var User
*/
public $user;
/**
* @inheritdoc
*/
public function run()
{
return $this->render('peopleIcons', [
'user' => $this->user
]);
}
}

View File

@ -0,0 +1,74 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
namespace humhub\modules\user\widgets;
use humhub\libs\Html;
use yii\helpers\Url;
use humhub\components\Widget;
/**
* PeopleTagList displays the user tags on the directory people page
*
* @since 1.2
* @author Luke
*/
class PeopleTagList extends Widget
{
/**
* @var \humhub\modules\user\models\User
*/
public $user;
/**
* @var int number of max. displayed tags
*/
public $maxTags = 5;
/**
* @var string Template for tags
*/
public $template = '{tags}';
/**
* @inheritdoc
*/
public function run()
{
$html = '';
$tags = $this->user->getTags();
$count = count($tags);
if ($count === 0) {
return $html;
}
if ($count > $this->maxTags) {
$tags = array_slice($tags, 0, $this->maxTags);
}
if (empty($tags)) {
return $html;
}
foreach ($tags as $tag) {
if (trim($tag) !== '') {
$html .= Html::a(Html::encode($tag), Url::to(['/user/people', 'keyword' => trim($tag)]), ['class' => 'label label-default']) . '&nbsp';
}
}
if ($html === '') {
return $html;
}
return str_replace('{tags}', $html, $this->template);
}
}

View File

@ -44,7 +44,7 @@ class UserFollowButton extends \yii\base\Widget
/**
* @var array options for unfollow button
*/
public $unfollowOptions = ['class' => 'btn btn-info'];
public $unfollowOptions = ['class' => 'btn btn-primary active'];
/**
* @inheritdoc
@ -52,10 +52,10 @@ class UserFollowButton extends \yii\base\Widget
public function init()
{
if ($this->followLabel === null) {
$this->followLabel = Yii::t("UserModule.base", "Follow");
$this->followLabel = Yii::t('UserModule.base', 'Follow');
}
if ($this->unfollowLabel === null) {
$this->unfollowLabel = Yii::t("UserModule.base", "Unfollow");
$this->unfollowLabel = '<span class="glyphicon glyphicon-ok"></span>&nbsp;&nbsp;' . Yii::t('UserModule.base', 'Following');
}
if (!isset($this->followOptions['class'])) {
@ -111,6 +111,11 @@ class UserFollowButton extends \yii\base\Widget
$this->followOptions['data-ui-loader'] = '';
$this->unfollowOptions['data-ui-loader'] = '';
// Confirm action "Unfollow"
$this->unfollowOptions['data-action-confirm'] = Yii::t('SpaceModule.base', 'Would you like to unfollow {userName}?', [
'{userName}' => '<strong>' . $this->user->getDisplayName() . '</strong>'
]);
$module = Yii::$app->getModule('user');
// still enable unfollow if following was disabled afterwards.

View File

@ -0,0 +1,53 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\libs\Html;
use humhub\modules\user\models\User;
use humhub\modules\user\widgets\PeopleActionButtons;
use humhub\modules\user\widgets\Image;
use humhub\modules\user\widgets\PeopleDetails;
use humhub\modules\user\widgets\PeopleIcons;
use humhub\modules\user\widgets\PeopleTagList;
use yii\web\View;
/* @var $this View */
/* @var $user User */
?>
<div class="card-panel">
<div class="card-bg-image"<?php if ($user->getProfileBannerImage()->hasImage()) : ?> style="background-image: url('<?= $user->getProfileBannerImage()->getUrl() ?>')"<?php endif; ?>></div>
<div class="card-header">
<?= Image::widget([
'user' => $user,
'htmlOptions' => ['class' => 'card-image-wrapper'],
'linkOptions' => ['data-contentcontainer-id' => $user->contentcontainer_id, 'class' => 'card-image-link'],
'width' => 94,
]); ?>
<?php /*<div class="card-icons">
<?= PeopleIcons::widget(['user' => $user]); ?>
</div> */ ?>
</div>
<div class="card-body">
<strong class="card-title"><?= Html::containerLink($user); ?></strong>
<?php if (trim($user->profile->title) !== '') : ?>
<div><?= Html::encode($user->profile->title); ?></div>
<?php endif; ?>
<?= PeopleDetails::widget([
'user' => $user,
'template' => '<div class="card-details">{lines}</div>',
'separator' => '<br>',
]); ?>
<?= PeopleTagList::widget([
'user' => $user,
'template' => '<div class="card-tags">{tags}</div>',
]); ?>
</div>
<?= PeopleActionButtons::widget([
'user' => $user,
'template' => '<div class="card-footer">{buttons}</div>',
]); ?>
</div>

View File

@ -0,0 +1,17 @@
<?php
/**
* @link https://www.humhub.org/
* @copyright Copyright (c) 2021 HumHub GmbH & Co. KG
* @license https://www.humhub.com/licences
*/
use humhub\modules\user\models\User;
use yii\web\View;
/* @var $this View */
/* @var $user User */
?>
<a href="#" class="fa fa-envelope-o"></a>
<a href="#" class="fa fa-comment-o"></a>
<a href="#" class="fa fa-mobile-phone"></a>

View File

@ -11,6 +11,6 @@ use tests\codeception\_support\BasePage;
class DirectoryMemberPage extends BasePage
{
public $route = 'directory/directory/members';
public $route = 'user/people';
}

View File

@ -11,11 +11,10 @@ use tests\codeception\_support\BasePage;
class DirectoryPage extends BasePage
{
public $route = 'directory/directory';
public $route = 'user/people';
public function clickMembers()
{
$this->actor->click('Members');
if($this->actor instanceof \AcceptanceTester) {
$this->actor->waitForText('Member directory', 30);
}

View File

@ -68,7 +68,7 @@ class AjaxButton extends Widget
$return = 'return false';
}
$this->view->registerJs("$('#{$this->htmlOptions['id']}').click(function() {
$this->view->registerJs("$('#{$this->htmlOptions['id']}').click(function(evt) {
$.ajax(" . Json::encode($this->ajaxOptions) . ");
{$return};
});");

View File

@ -0,0 +1,63 @@
humhub.module('directory', function(module, require, $) {
const client = require('client');
const loader = require('ui.loader');
const applyFilters = function(evt) {
$(evt.$trigger).closest('form').submit();
}
const loadMore = function(evt) {
const urlParams = {page: $(evt.$trigger).data('current-page') + 1};
client.get(module.config.loadMoreUrl, {data: urlParams}).then(function (response) {
$('.container-directory .card:hidden').show();
$('.container-directory .cards').append(response.response);
if (urlParams.page == $(evt.$trigger).data('total-pages')) {
// Remove button "Load more" because the last page was loaded
$(evt.$trigger).parent().remove();
} else {
$(evt.$trigger).data('current-page', urlParams.page);
hideLastNotCompletedRow();
}
}).catch(function(err) {
module.log.error(err, true);
reject();
}).finally(function() {
loader.reset(evt.$trigger);
});
}
const hideLastNotCompletedRow = function() {
const cardsNum = $('.container-directory .card').length;
if (!cardsNum) {
return;
}
const loadMoreButton = $('.directory-load-more button');
if (loadMoreButton.data('current-page') === loadMoreButton.data('total-pages')) {
// No reason to hide a not completed row if current page is last
return;
}
// Display button to load more cards
loadMoreButton.parent().show();
const cardsPerRow = Math.floor($('.container-directory .row').outerWidth() / $('.container-directory .card:first').width());
const hideLastCardsNum = cardsNum % cardsPerRow;
if (hideLastCardsNum > 0 && cardsNum > cardsPerRow) {
// Hide cards from not completed row
$('.container-directory .card').slice(-hideLastCardsNum).hide();
}
}
const init = function() {
hideLastNotCompletedRow();
$('input.form-search-filter[name=keyword]').focus();
}
module.export({
init,
applyFilters,
loadMore,
});
});

View File

@ -399,7 +399,11 @@ humhub.module('ui.additions', function (module, require, $) {
/**
* @deprecated since v1.2
*/
function setModalLoader() {
$(".modal-footer .btn").hide();
$(".modal-footer .loader").removeClass("hidden");
}
function setModalLoader(evt) {
var modalFooter = $('.modal-footer');
if (typeof evt === 'object') {
modalFooter = $(evt.target).closest('.modal-footer');
}
modalFooter.find('.btn').hide();
modalFooter.find('.loader').removeClass('hidden');
}

View File

@ -85,7 +85,20 @@
&:active,
&.active {
outline: 0;
background: darken(@primary, 5%) !important;
border: 1px solid @primary;
padding: 7px 15px;
color: @primary !important;
background: @text-color-contrast !important;
box-shadow: none;
&.btn-sm {
padding: 3px 7px;
}
}
&.active:hover,
&.active:focus {
border: 1px solid darken(@primary, 5%);
color: darken(@primary, 5%) !important;
}
}
@ -117,7 +130,20 @@
&:active,
&.active {
outline: 0;
background: darken(@info, 5%);
border: 1px solid @info;
padding: 7px 15px;
color: @info !important;
background: @text-color-contrast !important;
box-shadow: none;
&.btn-sm {
padding: 3px 7px;
}
}
&.active:hover,
&.active:focus {
border: 1px solid darken(@info, 5%);
color: darken(@info, 5%) !important;
}
}

184
static/less/directory.less Normal file
View File

@ -0,0 +1,184 @@
.container-directory {
&.container-fluid {
@media (min-width: 500px) {
.card {
width: 50%;
}
}
@media (min-width: 1000px) {
.card {
width: 33.33333333%;
}
}
@media (min-width: 1300px) {
.card {
width: 25%;
}
}
@media (min-width: 1600px) {
.card {
width: 20%;
}
}
@media (min-width: 1900px) {
.card {
width: 16.66666667%;
}
}
}
.form-search {
.row > div {
padding-bottom: 3px;
}
.form-search-filter-keyword {
position: relative;
.form-button-search {
position: absolute;
right: 18px;
margin-bottom: 3px;
}
}
.form-control.form-search-filter {
width: 100%;
height: 40px;
margin: 3px 0 0;
padding: 8px 30px 10px 8px;
border-radius: 4px;
border: solid 1px #c5c5c5;
}
.form-button-search {
background: none;
border: 0;
font-size: 16px;
color: #000;
top: initial;
bottom: 9px;
}
.form-search-field-info {
font-size: 80%;
}
}
.form-search-reset {
text-decoration: underline;
display: block;
margin-top: 26px;
&:hover {
text-decoration: none;
}
}
.cards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.card {
display: flex;
flex-direction: row;
.card-panel {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
margin: 15px 0;
border-radius: 4px;
background-color: #ffffff;
}
.card-icons .fa {
color: #21a1b3;
span {
font: 12px 'Open Sans', sans-serif;
font-weight: 600;
}
}
.card-bg-image {
width: 100%;
height: 86px;
background-color: #cfcfcf;
background-size: cover;
background-position: center;
border-radius: 4px 4px 0 0;
}
.card-header {
position: absolute;
padding: 16px;
display: table;
width: 100%;
.card-image-wrapper {
display: table-cell;
width: 98px;
}
.card-image-link {
display: inline-block;
border: 2px solid #fff;
border-radius: 6px;
}
.card-icons {
display: table-cell;
padding: 0 0 2px 5px;
text-align: right;
vertical-align: bottom;
font-size: 16px;
.fa {
color: #21a1b3;
&.fa-mobile-phone {
font-size: 22px;
line-height: 15px;
position: relative;
top: 2px;
}
}
}
}
.card-body {
flex-grow: 1;
padding: 44px 16px 24px 16px;
overflow: auto;
.card-title {
font-size: 16px;
font-weight: bold;
line-height: 24px;
}
.card-details {
margin-top: 8px;
color: #57646c;
a {
color: #21a1b3;
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
.card-tags {
margin-top: 20px;
}
}
.card-footer {
padding: 0 16px 20px;
a.btn {
float: left;
margin: 0 8px 4px 0;
white-space: normal;
hyphens: none;
&:last-child {
margin-right: 0;
}
}
.btn-group a.btn {
margin-right: 0;
}
}
}
.directory-load-more {
display: none;
text-align: center;
padding: 12px 0 15px;
}
}

View File

@ -204,6 +204,11 @@
@import "print.less";
}
@prev-directory: false;
& when not(@prev-directory) {
@import "directory.less";
}
@import "../css/select2Theme/build.less";
// LEGACY/DEPRECATED User- & Space picker

File diff suppressed because one or more lines are too long