mirror of https://github.com/sualko/cloud_bbb
Merge pull request #67 from sualko/feat-restrictions
feat: add admin setting to restrict roomspull/75/head
commit
2c5ff0dbb7
|
@ -4,6 +4,7 @@ return [
|
||||||
'resources' => [
|
'resources' => [
|
||||||
'room' => ['url' => '/rooms'],
|
'room' => ['url' => '/rooms'],
|
||||||
'roomShare' => ['url' => '/roomShares'],
|
'roomShare' => ['url' => '/roomShares'],
|
||||||
|
'restriction' => ['url' => '/restrictions'],
|
||||||
],
|
],
|
||||||
'routes' => [
|
'routes' => [
|
||||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||||
|
@ -12,5 +13,6 @@ return [
|
||||||
['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'],
|
['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'],
|
||||||
['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'],
|
['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'],
|
||||||
['name' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'],
|
['name' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'],
|
||||||
|
['name' => 'restriction#user', 'url' => '/restrictions/user', 'verb' => 'GET'],
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
|
@ -120,10 +120,9 @@ class JoinController extends Controller {
|
||||||
\OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]);
|
\OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]);
|
||||||
|
|
||||||
return new TemplateResponse($this->appName, 'forward', [
|
return new TemplateResponse($this->appName, 'forward', [
|
||||||
'room' => $room->name,
|
'room' => $room->name,
|
||||||
'url' => $joinUrl,
|
'url' => $joinUrl,
|
||||||
], 'guest');
|
], 'guest');
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getRoom(): ?Room {
|
private function getRoom(): ?Room {
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Controller;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\Db\Restriction;
|
||||||
|
use OCP\IRequest;
|
||||||
|
use OCP\IGroupManager;
|
||||||
|
use OCP\IUserManager;
|
||||||
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
|
use OCP\AppFramework\Controller;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\Service\RestrictionService;
|
||||||
|
|
||||||
|
class RestrictionController extends Controller {
|
||||||
|
/** @var RestrictionService */
|
||||||
|
private $service;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
private $userId;
|
||||||
|
|
||||||
|
/** @var IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var IGroupManager */
|
||||||
|
private $groupManager;
|
||||||
|
|
||||||
|
use Errors;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
$appName,
|
||||||
|
IRequest $request,
|
||||||
|
RestrictionService $service,
|
||||||
|
IUserManager $userManager,
|
||||||
|
IGroupManager $groupManager,
|
||||||
|
$userId
|
||||||
|
) {
|
||||||
|
parent::__construct($appName, $request);
|
||||||
|
$this->service = $service;
|
||||||
|
$this->userManager = $userManager;
|
||||||
|
$this->groupManager = $groupManager;
|
||||||
|
$this->userId = $userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
public function user(): DataResponse {
|
||||||
|
$user = $this->userManager->get($this->userId);
|
||||||
|
$groupIds = $this->groupManager->getUserGroupIds($user);
|
||||||
|
|
||||||
|
return new DataResponse($this->service->findByGroupIds($groupIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): DataResponse {
|
||||||
|
$restrictions = $this->service->findAll();
|
||||||
|
|
||||||
|
if (!$this->service->existsByGroupId(Restriction::ALL_ID)) {
|
||||||
|
$defaultRestriction = new Restriction();
|
||||||
|
$defaultRestriction->setGroupId('');
|
||||||
|
|
||||||
|
$restrictions[] = $defaultRestriction;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataResponse($restrictions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(
|
||||||
|
string $groupId
|
||||||
|
): DataResponse {
|
||||||
|
if ($this->service->existsByGroupId($groupId)) {
|
||||||
|
return new DataResponse(null, Http::STATUS_CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DataResponse($this->service->create(
|
||||||
|
$groupId
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(
|
||||||
|
int $id,
|
||||||
|
string $groupId,
|
||||||
|
int $maxRooms,
|
||||||
|
array $roomTypes,
|
||||||
|
int $maxParticipants,
|
||||||
|
bool $allowRecording
|
||||||
|
): DataResponse {
|
||||||
|
return $this->handleNotFound(function () use (
|
||||||
|
$id,
|
||||||
|
$groupId,
|
||||||
|
$maxRooms,
|
||||||
|
$roomTypes,
|
||||||
|
$maxParticipants,
|
||||||
|
$allowRecording) {
|
||||||
|
return $this->service->update(
|
||||||
|
$id,
|
||||||
|
$groupId,
|
||||||
|
$maxRooms,
|
||||||
|
$roomTypes,
|
||||||
|
$maxParticipants,
|
||||||
|
$allowRecording
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(int $id): DataResponse {
|
||||||
|
return $this->handleNotFound(function () use ($id) {
|
||||||
|
$roomShare = $this->service->find($id);
|
||||||
|
|
||||||
|
return $this->service->delete($id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ namespace OCA\BigBlueButton\Controller;
|
||||||
|
|
||||||
use OCA\BigBlueButton\Service\RoomService;
|
use OCA\BigBlueButton\Service\RoomService;
|
||||||
use OCA\BigBlueButton\Permission;
|
use OCA\BigBlueButton\Permission;
|
||||||
|
use OCA\BigBlueButton\Db\Room;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\DataResponse;
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
|
@ -63,13 +64,34 @@ class RoomController extends Controller {
|
||||||
string $name,
|
string $name,
|
||||||
string $welcome,
|
string $welcome,
|
||||||
int $maxParticipants,
|
int $maxParticipants,
|
||||||
bool $record
|
bool $record,
|
||||||
|
string $access
|
||||||
): DataResponse {
|
): DataResponse {
|
||||||
|
if (!$this->permission->isAllowedToCreateRoom($this->userId)) {
|
||||||
|
return new DataResponse(null, Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$restriction = $this->permission->getRestriction($this->userId);
|
||||||
|
|
||||||
|
if ($restriction->getMaxParticipants() > -1 && ($maxParticipants > $restriction->getMaxParticipants() || $maxParticipants <= 0)) {
|
||||||
|
return new DataResponse('Max participants limit exceeded.', Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$restriction->getAllowRecording() && $record) {
|
||||||
|
return new DataResponse('Not allowed to enable recordings.', Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$disabledRoomTypes = \json_decode($restriction->getRoomTypes());
|
||||||
|
if (in_array($access, $disabledRoomTypes) || !in_array($access, Room::ACCESS)) {
|
||||||
|
return new DataResponse('Access type not allowed.', Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
return new DataResponse($this->service->create(
|
return new DataResponse($this->service->create(
|
||||||
$name,
|
$name,
|
||||||
$welcome,
|
$welcome,
|
||||||
$maxParticipants,
|
$maxParticipants,
|
||||||
$record,
|
$record,
|
||||||
|
$access,
|
||||||
$this->userId
|
$this->userId
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -92,6 +114,21 @@ class RoomController extends Controller {
|
||||||
return new DataResponse(null, Http::STATUS_FORBIDDEN);
|
return new DataResponse(null, Http::STATUS_FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$restriction = $this->permission->getRestriction($this->userId);
|
||||||
|
|
||||||
|
if ($restriction->getMaxParticipants() > -1 && $maxParticipants !== $room->getMaxParticipants() && ($maxParticipants > $restriction->getMaxParticipants() || $maxParticipants <= 0)) {
|
||||||
|
return new DataResponse('Max participants limit exceeded.', Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$restriction->getAllowRecording() && $record !== $room->getRecord()) {
|
||||||
|
return new DataResponse('Not allowed to enable recordings.', Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$disabledRoomTypes = \json_decode($restriction->getRoomTypes());
|
||||||
|
if ((in_array($access, $disabledRoomTypes) && $access !== $room->getAccess()) || !in_array($access, Room::ACCESS)) {
|
||||||
|
return new DataResponse('Access type not allowed.', Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $everyoneIsModerator, $access) {
|
return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $everyoneIsModerator, $access) {
|
||||||
return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator);
|
return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Db;
|
||||||
|
|
||||||
|
use JsonSerializable;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Db\Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method int getRoomId()
|
||||||
|
* @method int getMaxRooms()
|
||||||
|
* @method string getRoomTypes()
|
||||||
|
* @method int getMaxParticipants()
|
||||||
|
* @method bool getAllowRecording()
|
||||||
|
* @method void setRoomId(string $id)
|
||||||
|
* @method void setMaxRooms(int $number)
|
||||||
|
* @method void setMaxParticipants(int $number)
|
||||||
|
* @method void setAllowRecording(bool $allow)
|
||||||
|
*/
|
||||||
|
class Restriction extends Entity implements JsonSerializable {
|
||||||
|
public const ALL_ID = '';
|
||||||
|
|
||||||
|
protected $groupId;
|
||||||
|
protected $maxRooms = -1;
|
||||||
|
protected $roomTypes = '[]';
|
||||||
|
protected $maxParticipants = -1;
|
||||||
|
protected $allowRecording = true;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->addType('max_rooms', 'integer');
|
||||||
|
$this->addType('max_participants', 'integer');
|
||||||
|
$this->addType('allow_recording', 'boolean');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jsonSerialize(): array {
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'groupId' => $this->groupId,
|
||||||
|
'maxRooms' => (int) $this->maxRooms,
|
||||||
|
'roomTypes' => \json_decode($this->roomTypes),
|
||||||
|
'maxParticipants' => (int) $this->maxParticipants,
|
||||||
|
'allowRecording' => boolval($this->allowRecording),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Db;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
|
use OCP\AppFramework\Db\QBMapper;
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\IDBConnection;
|
||||||
|
|
||||||
|
class RestrictionMapper extends QBMapper {
|
||||||
|
public function __construct(IDBConnection $db) {
|
||||||
|
parent::__construct($db, 'bbb_restrictions', Restriction::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||||
|
* @throws DoesNotExistException
|
||||||
|
*/
|
||||||
|
public function find(int $id): Restriction {
|
||||||
|
/* @var $qb IQueryBuilder */
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->select('*')
|
||||||
|
->from($this->tableName)
|
||||||
|
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
|
||||||
|
|
||||||
|
return $this->findEntity($qb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||||
|
* @throws DoesNotExistException
|
||||||
|
*/
|
||||||
|
public function findByGroupId(string $groupId): Restriction {
|
||||||
|
/* @var $qb IQueryBuilder */
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->select('*')
|
||||||
|
->from($this->tableName)
|
||||||
|
->where($qb->expr()->eq('group_id', $qb->createNamedParameter($groupId)));
|
||||||
|
|
||||||
|
return $this->findEntity($qb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<Restriction>
|
||||||
|
*/
|
||||||
|
public function findByGroupIds(array $groupIds): array {
|
||||||
|
/* @var $qb IQueryBuilder */
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->select('*')
|
||||||
|
->from($this->tableName)
|
||||||
|
->where($qb->expr()->in('group_id', $qb->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY)));
|
||||||
|
|
||||||
|
/** @var array<Restriction> */
|
||||||
|
return $this->findEntities($qb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<Restriction>
|
||||||
|
*/
|
||||||
|
public function findAll(): array {
|
||||||
|
/* @var $qb IQueryBuilder */
|
||||||
|
$qb = $this->db->getQueryBuilder();
|
||||||
|
$qb->select('*')
|
||||||
|
->from($this->tableName);
|
||||||
|
|
||||||
|
/** @var array<Restriction> */
|
||||||
|
return $this->findEntities($qb);
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,8 @@ class Room extends Entity implements JsonSerializable {
|
||||||
public const ACCESS_INTERNAL = 'internal';
|
public const ACCESS_INTERNAL = 'internal';
|
||||||
public const ACCESS_INTERNAL_RESTRICTED = 'internal_restricted';
|
public const ACCESS_INTERNAL_RESTRICTED = 'internal_restricted';
|
||||||
|
|
||||||
|
public const ACCESS = [self::ACCESS_PUBLIC, self::ACCESS_PASSWORD, self::ACCESS_WAITING_ROOM, self::ACCESS_INTERNAL, self::ACCESS_INTERNAL_RESTRICTED];
|
||||||
|
|
||||||
public $uid;
|
public $uid;
|
||||||
public $name;
|
public $name;
|
||||||
public $attendeePassword;
|
public $attendeePassword;
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Migration;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use OCP\DB\ISchemaWrapper;
|
||||||
|
use OCP\Migration\IOutput;
|
||||||
|
use OCP\Migration\SimpleMigrationStep;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated migration step: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
class Version000000Date20200826100844 extends SimpleMigrationStep {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IOutput $output
|
||||||
|
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||||
|
* @param array $options
|
||||||
|
* @return null|ISchemaWrapper
|
||||||
|
*/
|
||||||
|
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
|
||||||
|
/** @var ISchemaWrapper $schema */
|
||||||
|
$schema = $schemaClosure();
|
||||||
|
|
||||||
|
if (!$schema->hasTable('bbb_restrictions')) {
|
||||||
|
$table = $schema->createTable('bbb_restrictions');
|
||||||
|
$table->addColumn('id', 'integer', [
|
||||||
|
'autoincrement' => true,
|
||||||
|
'notnull' => true,
|
||||||
|
]);
|
||||||
|
$table->addColumn('group_id', 'string', [
|
||||||
|
'unique' => true,
|
||||||
|
'notnull' => true,
|
||||||
|
'length' => 200,
|
||||||
|
]);
|
||||||
|
$table->addColumn('max_rooms', 'integer', [
|
||||||
|
'notnull' => false,
|
||||||
|
'default' => -1,
|
||||||
|
]);
|
||||||
|
$table->addColumn('room_types', 'string', [
|
||||||
|
'notnull' => true,
|
||||||
|
'default' => '[]',
|
||||||
|
]);
|
||||||
|
$table->addColumn('max_participants', 'integer', [
|
||||||
|
'notnull' => false,
|
||||||
|
'default' => -1,
|
||||||
|
]);
|
||||||
|
$table->addColumn('allow_recording', 'boolean', [
|
||||||
|
'notnull' => true,
|
||||||
|
'default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table->setPrimaryKey(['id']);
|
||||||
|
$table->addIndex(['group_id'], 'restrictions_group_id_index');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,27 +3,60 @@
|
||||||
namespace OCA\BigBlueButton;
|
namespace OCA\BigBlueButton;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use OCA\BigBlueButton\Service\RoomService;
|
||||||
|
use OCA\BigBlueButton\Service\RestrictionService;
|
||||||
use OCA\BigBlueButton\Service\RoomShareService;
|
use OCA\BigBlueButton\Service\RoomShareService;
|
||||||
use OCA\BigBlueButton\Db\Room;
|
use OCA\BigBlueButton\Db\Room;
|
||||||
use OCA\BigBlueButton\Db\RoomShare;
|
use OCA\BigBlueButton\Db\RoomShare;
|
||||||
|
use OCA\BigBlueButton\Db\Restriction;
|
||||||
|
use OCP\IUserManager;
|
||||||
use OCP\IGroupManager;
|
use OCP\IGroupManager;
|
||||||
|
|
||||||
class Permission {
|
class Permission {
|
||||||
|
|
||||||
|
/** @var IUserManager */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
/** @var IGroupManager */
|
/** @var IGroupManager */
|
||||||
private $groupManager;
|
private $groupManager;
|
||||||
|
|
||||||
|
/** @var RoomService */
|
||||||
|
private $roomService;
|
||||||
|
|
||||||
|
/** @var RestrictionService */
|
||||||
|
private $restrictionService;
|
||||||
|
|
||||||
/** @var RoomShareService */
|
/** @var RoomShareService */
|
||||||
private $roomShareService;
|
private $roomShareService;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
IUserManager $userManager,
|
||||||
IGroupManager $groupManager,
|
IGroupManager $groupManager,
|
||||||
|
RoomService $roomService,
|
||||||
|
RestrictionService $restrictionService,
|
||||||
RoomShareService $roomShareService
|
RoomShareService $roomShareService
|
||||||
) {
|
) {
|
||||||
|
$this->userManager = $userManager;
|
||||||
$this->groupManager = $groupManager;
|
$this->groupManager = $groupManager;
|
||||||
|
$this->roomService = $roomService;
|
||||||
|
$this->restrictionService = $restrictionService;
|
||||||
$this->roomShareService = $roomShareService;
|
$this->roomShareService = $roomShareService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRestriction(string $uid): Restriction {
|
||||||
|
$user = $this->userManager->get($uid);
|
||||||
|
$groupIds = $this->groupManager->getUserGroupIds($user);
|
||||||
|
|
||||||
|
return $this->restrictionService->findByGroupIds($groupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAllowedToCreateRoom(string $uid) {
|
||||||
|
$numberOfCreatedRooms = count($this->roomService->findAll($uid, []));
|
||||||
|
$restriction = $this->getRestriction($uid);
|
||||||
|
|
||||||
|
return $restriction->getMaxRooms() < 0 || $restriction->getMaxRooms() > $numberOfCreatedRooms;
|
||||||
|
}
|
||||||
|
|
||||||
public function isUser(Room $room, ?string $uid) {
|
public function isUser(Room $room, ?string $uid) {
|
||||||
return $this->hasPermission($room, $uid, function (RoomShare $share) {
|
return $this->hasPermission($room, $uid, function (RoomShare $share) {
|
||||||
return $share->hasUserPermission();
|
return $share->hasUserPermission();
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Service;
|
||||||
|
|
||||||
|
class RestrictionNotFound extends \Exception {
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Service;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Db\DoesNotExistException;
|
||||||
|
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\Db\Restriction;
|
||||||
|
use OCA\BigBlueButton\Db\RestrictionMapper;
|
||||||
|
|
||||||
|
class RestrictionService {
|
||||||
|
/** @var RestrictionMapper */
|
||||||
|
private $mapper;
|
||||||
|
|
||||||
|
public function __construct(RestrictionMapper $mapper) {
|
||||||
|
$this->mapper = $mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAll(): array {
|
||||||
|
return $this->mapper->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function existsByGroupId(string $groupId): bool {
|
||||||
|
try {
|
||||||
|
$this->mapper->findByGroupId($groupId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (DoesNotExistException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByGroupIds(array $groupIds): Restriction {
|
||||||
|
$restrictions = $this->mapper->findByGroupIds($groupIds);
|
||||||
|
try {
|
||||||
|
$restriction = $this->mapper->findByGroupId(Restriction::ALL_ID);
|
||||||
|
} catch (DoesNotExistException $e) {
|
||||||
|
$restriction = new Restriction();
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomTypes = \json_decode($restriction->getRoomTypes());
|
||||||
|
|
||||||
|
foreach ($restrictions as $r) {
|
||||||
|
if ($restriction->getMaxRooms() > -1 && ($r->getMaxRooms() === -1 || $restriction->getMaxRooms() < $r->getMaxRooms())) {
|
||||||
|
$restriction->setMaxRooms($r->getMaxRooms());
|
||||||
|
}
|
||||||
|
|
||||||
|
$rRoomTypes = \json_decode($r->getRoomTypes());
|
||||||
|
$roomTypes = array_intersect($roomTypes, $rRoomTypes);
|
||||||
|
|
||||||
|
if ($restriction->getMaxParticipants() > -1 && ($r->getMaxParticipants() === -1 || $restriction->getMaxParticipants() < $r->getMaxParticipants())) {
|
||||||
|
$restriction->setMaxParticipants($r->getMaxParticipants());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$restriction->getAllowRecording() && $r->getAllowRecording()) {
|
||||||
|
$restriction->setAllowRecording($r->getAllowRecording());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$restriction->setId(0);
|
||||||
|
$restriction->setGroupId('__cumulative');
|
||||||
|
$restriction->setRoomTypes(\json_encode(\array_values($roomTypes)));
|
||||||
|
|
||||||
|
return $restriction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find($id): Restriction {
|
||||||
|
try {
|
||||||
|
return $this->mapper->find($id);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->handleException($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(string $groupId): Restriction {
|
||||||
|
$restriction = new Restriction();
|
||||||
|
|
||||||
|
$restriction->setGroupId($groupId);
|
||||||
|
|
||||||
|
return $this->mapper->insert($restriction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(int $id, string $groupId, int $maxRooms, array $roomTypes, int $maxParticipants, bool $allowRecording): Restriction {
|
||||||
|
try {
|
||||||
|
$restriction = $this->mapper->find($id);
|
||||||
|
|
||||||
|
$restriction->setGroupId($groupId);
|
||||||
|
$restriction->setMaxRooms(\max($maxRooms, -1));
|
||||||
|
$restriction->setRoomTypes(\json_encode($roomTypes));
|
||||||
|
$restriction->setMaxParticipants(\max($maxParticipants, -1));
|
||||||
|
$restriction->setAllowRecording($allowRecording);
|
||||||
|
|
||||||
|
return $this->mapper->update($restriction);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->handleException($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(int $id): Restriction {
|
||||||
|
try {
|
||||||
|
$restriction = $this->mapper->find($id);
|
||||||
|
$this->mapper->delete($restriction);
|
||||||
|
|
||||||
|
return $restriction;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->handleException($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleException(Exception $e): void {
|
||||||
|
if ($e instanceof DoesNotExistException ||
|
||||||
|
$e instanceof MultipleObjectsReturnedException) {
|
||||||
|
throw new RestrictionNotFound($e->getMessage());
|
||||||
|
} else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,16 +57,17 @@ class RoomService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create($name, $welcome, $maxParticipants, $record, $userId) {
|
public function create($name, $welcome, $maxParticipants, $record, $access, $userId) {
|
||||||
$room = new Room();
|
$room = new Room();
|
||||||
|
|
||||||
$room->setUid(\OC::$server->getSecureRandom()->generate(16, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
$room->setUid(\OC::$server->getSecureRandom()->generate(16, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||||
$room->setName($name);
|
$room->setName($name);
|
||||||
$room->setWelcome($welcome);
|
$room->setWelcome($welcome);
|
||||||
$room->setMaxParticipants($maxParticipants);
|
$room->setMaxParticipants(\max($maxParticipants, 0));
|
||||||
$room->setAttendeePassword(\OC::$server->getSecureRandom()->generate(32, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
$room->setAttendeePassword(\OC::$server->getSecureRandom()->generate(32, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||||
$room->setModeratorPassword(\OC::$server->getSecureRandom()->generate(32, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
$room->setModeratorPassword(\OC::$server->getSecureRandom()->generate(32, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||||
$room->setRecord($record);
|
$room->setRecord($record);
|
||||||
|
$room->setAccess($access);
|
||||||
$room->setUserId($userId);
|
$room->setUserId($userId);
|
||||||
|
|
||||||
return $this->mapper->insert($room);
|
return $this->mapper->insert($room);
|
||||||
|
@ -82,7 +83,7 @@ class RoomService {
|
||||||
|
|
||||||
$room->setName($name);
|
$room->setName($name);
|
||||||
$room->setWelcome($welcome);
|
$room->setWelcome($welcome);
|
||||||
$room->setMaxParticipants($maxParticipants);
|
$room->setMaxParticipants(\max($maxParticipants, 0));
|
||||||
$room->setRecord($record);
|
$room->setRecord($record);
|
||||||
$room->setAccess($access);
|
$room->setAccess($access);
|
||||||
$room->setEveryoneIsModerator($everyoneIsModerator);
|
$room->setEveryoneIsModerator($everyoneIsModerator);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
/** @var $_ array */
|
/** @var $_ array */
|
||||||
|
|
||||||
script('bbb', 'admin');
|
script('bbb', 'admin');
|
||||||
|
script('bbb', 'restrictions');
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div id="bbb-settings" class="section">
|
<div id="bbb-settings" class="section">
|
||||||
|
@ -19,7 +20,11 @@ script('bbb', 'admin');
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" name="app.navigation" id="bbb-app-navigation" class="checkbox" value="1" <?php p($_['app.navigation']); ?>>
|
<input type="checkbox" name="app.navigation" id="bbb-app-navigation" class="checkbox" value="1" <?php p($_['app.navigation']); ?> />
|
||||||
<label for="bbb-app-navigation"><?php p($l->t('Show room manager in app navigation instead of settings page.')); ?></label>
|
<label for="bbb-app-navigation"><?php p($l->t('Show room manager in app navigation instead of settings page.')); ?></label>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3>Restrictions</h3>
|
||||||
|
<div id="bbb-restrictions">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,9 +4,14 @@ namespace OCA\BigBlueButton\Tests;
|
||||||
|
|
||||||
use OCA\BigBlueButton\Db\Room;
|
use OCA\BigBlueButton\Db\Room;
|
||||||
use OCA\BigBlueButton\Db\RoomShare;
|
use OCA\BigBlueButton\Db\RoomShare;
|
||||||
|
use OCA\BigBlueButton\Db\Restriction;
|
||||||
use OCA\BigBlueButton\Permission;
|
use OCA\BigBlueButton\Permission;
|
||||||
|
use OCA\BigBlueButton\Service\RoomService;
|
||||||
use OCA\BigBlueButton\Service\RoomShareService;
|
use OCA\BigBlueButton\Service\RoomShareService;
|
||||||
|
use OCA\BigBlueButton\Service\RestrictionService;
|
||||||
|
use OCP\IUserManager;
|
||||||
use OCP\IGroupManager;
|
use OCP\IGroupManager;
|
||||||
|
use OCP\IUser;
|
||||||
use PHPUnit\Framework\MockObject\MockObject;
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
@ -14,27 +19,79 @@ class PermissionTest extends TestCase {
|
||||||
/** @var Permission */
|
/** @var Permission */
|
||||||
private $permission;
|
private $permission;
|
||||||
|
|
||||||
|
/** @var IUserManager|MockObject */
|
||||||
|
private $userManager;
|
||||||
|
|
||||||
/** @var IGroupManager|MockObject */
|
/** @var IGroupManager|MockObject */
|
||||||
private $groupManager;
|
private $groupManager;
|
||||||
|
|
||||||
|
/** @var RoomService|MockObject */
|
||||||
|
private $roomService;
|
||||||
|
|
||||||
/** @var RoomShareService|MockObject */
|
/** @var RoomShareService|MockObject */
|
||||||
private $roomShareService;
|
private $roomShareService;
|
||||||
|
|
||||||
|
/** @var RestrictionService|MockObject */
|
||||||
|
private $restrictionService;
|
||||||
|
|
||||||
public function setUp(): void {
|
public function setUp(): void {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
|
/** @var IUserManager|MockObject */
|
||||||
|
$this->userManager = $this->createMock(IUserManager::class);
|
||||||
|
|
||||||
/** @var IGroupManager|MockObject */
|
/** @var IGroupManager|MockObject */
|
||||||
$this->groupManager = $this->createMock(IGroupManager::class);
|
$this->groupManager = $this->createMock(IGroupManager::class);
|
||||||
|
|
||||||
|
/** @var RoomService|MockObject */
|
||||||
|
$this->roomService = $this->createMock(RoomService::class);
|
||||||
|
|
||||||
|
/** @var RestrictionService|MockObject */
|
||||||
|
$this->restrictionService = $this->createMock(RestrictionService::class);
|
||||||
|
|
||||||
/** @var RoomShareService|MockObject */
|
/** @var RoomShareService|MockObject */
|
||||||
$this->roomShareService = $this->createMock(RoomShareService::class);
|
$this->roomShareService = $this->createMock(RoomShareService::class);
|
||||||
|
|
||||||
$this->permission = new Permission(
|
$this->permission = new Permission(
|
||||||
|
$this->userManager,
|
||||||
$this->groupManager,
|
$this->groupManager,
|
||||||
|
$this->roomService,
|
||||||
|
$this->restrictionService,
|
||||||
$this->roomShareService
|
$this->roomShareService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testIsUserNotAllowed() {
|
||||||
|
$this->userManager
|
||||||
|
->expects($this->once())
|
||||||
|
->method('get')
|
||||||
|
->with('foobar')
|
||||||
|
->willReturn($this->createMock(IUser::class));
|
||||||
|
|
||||||
|
$this->groupManager
|
||||||
|
->expects($this->once())
|
||||||
|
->method('getUserGroupIds')
|
||||||
|
->willReturn([]);
|
||||||
|
|
||||||
|
$this->roomService
|
||||||
|
->expects($this->once())
|
||||||
|
->method('findAll')
|
||||||
|
->willReturn([
|
||||||
|
$this->createRoom(1, 'foo'),
|
||||||
|
$this->createRoom(2, 'bar'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$restriction = new Restriction();
|
||||||
|
$restriction->setMaxRooms(2);
|
||||||
|
|
||||||
|
$this->restrictionService
|
||||||
|
->expects($this->once())
|
||||||
|
->method('findByGroupIds')
|
||||||
|
->willReturn($restriction);
|
||||||
|
|
||||||
|
$this->assertFalse($this->permission->isAllowedToCreateRoom('foobar'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testIsUser() {
|
public function testIsUser() {
|
||||||
$room = $this->createRoom(1, 'foo');
|
$room = $this->createRoom(1, 'foo');
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Tests\Unit\Service;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\Service\RestrictionService;
|
||||||
|
use OCA\BigBlueButton\Db\Room;
|
||||||
|
use OCA\BigBlueButton\Db\Restriction;
|
||||||
|
use OCA\BigBlueButton\Db\RestrictionMapper;
|
||||||
|
|
||||||
|
class RestrictionServiceTest extends TestCase {
|
||||||
|
protected $mapper;
|
||||||
|
protected $service;
|
||||||
|
|
||||||
|
public function setUp(): void {
|
||||||
|
$this->mapper = $this->createMock(RestrictionMapper::class);
|
||||||
|
|
||||||
|
$this->service = new RestrictionService($this->mapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindByGroupIds() {
|
||||||
|
$restriction0 = new Restriction();
|
||||||
|
$restriction0->setRoomTypes(\json_encode([Room::ACCESS_INTERNAL]));
|
||||||
|
$restriction0->setMaxParticipants(50);
|
||||||
|
$restriction0->setAllowRecording(false);
|
||||||
|
|
||||||
|
$restriction1 = new Restriction();
|
||||||
|
$restriction1->setRoomTypes(\json_encode([Room::ACCESS_INTERNAL, Room::ACCESS_INTERNAL_RESTRICTED]));
|
||||||
|
$restriction1->setMaxRooms(10);
|
||||||
|
$restriction1->setMaxParticipants(100);
|
||||||
|
$restriction1->setAllowRecording(true);
|
||||||
|
|
||||||
|
$this->mapper
|
||||||
|
->expects($this->once())
|
||||||
|
->method('findByGroupIds')
|
||||||
|
->willReturn([$restriction1]);
|
||||||
|
|
||||||
|
$this->mapper
|
||||||
|
->expects($this->once())
|
||||||
|
->method('findByGroupId')
|
||||||
|
->willReturn($restriction0);
|
||||||
|
|
||||||
|
$result = $this->service->findByGroupIds([]);
|
||||||
|
|
||||||
|
$this->assertEquals([Room::ACCESS_INTERNAL], \json_decode($result->getRoomTypes()));
|
||||||
|
$this->assertEquals(-1, $result->getMaxRooms());
|
||||||
|
$this->assertEquals(100, $result->getMaxParticipants());
|
||||||
|
$this->assertTrue($result->getAllowRecording());
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,15 @@ export enum Access {
|
||||||
InternalRestricted = 'internal_restricted',
|
InternalRestricted = 'internal_restricted',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Restriction {
|
||||||
|
id: number;
|
||||||
|
groupId: string;
|
||||||
|
maxRooms: number;
|
||||||
|
roomTypes: string[];
|
||||||
|
maxParticipants: number;
|
||||||
|
allowRecording: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Room {
|
export interface Room {
|
||||||
id: number;
|
id: number;
|
||||||
uid: string;
|
uid: string;
|
||||||
|
@ -65,6 +74,46 @@ class Api {
|
||||||
return OC.generateUrl(`apps/bbb/${endpoint}`);
|
return OC.generateUrl(`apps/bbb/${endpoint}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getRestriction(): Promise<Restriction> {
|
||||||
|
const response = await axios.get(this.getUrl('restrictions/user'));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRestrictions(): Promise<Restriction[]> {
|
||||||
|
const response = await axios.get(this.getUrl('restrictions'));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createRestriction(groupId: string) {
|
||||||
|
const response = await axios.post(this.getUrl('restrictions'), {
|
||||||
|
groupId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateRestriction(restriction: Restriction) {
|
||||||
|
if (!restriction.id) {
|
||||||
|
const newRestriction = await this.createRestriction(
|
||||||
|
restriction.groupId
|
||||||
|
);
|
||||||
|
|
||||||
|
restriction.id = newRestriction.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.put(this.getUrl(`restrictions/${restriction.id}`), restriction);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteRestriction(id: number) {
|
||||||
|
const response = await axios.delete(this.getUrl(`restrictions/${id}`));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
public getRoomUrl(room: Room) {
|
public getRoomUrl(room: Room) {
|
||||||
return window.location.origin + api.getUrl(`b/${room.uid}`);
|
return window.location.origin + api.getUrl(`b/${room.uid}`);
|
||||||
}
|
}
|
||||||
|
@ -75,12 +124,13 @@ class Api {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createRoom(name: string) {
|
public async createRoom(name: string, access: Access = Access.Public, maxParticipants = 0) {
|
||||||
const response = await axios.post(this.getUrl('rooms'), {
|
const response = await axios.post(this.getUrl('rooms'), {
|
||||||
name,
|
name,
|
||||||
welcome: '',
|
welcome: '',
|
||||||
maxParticipants: 0,
|
maxParticipants,
|
||||||
record: false,
|
record: false,
|
||||||
|
access,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
@ -165,10 +215,11 @@ class Api {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecommendedShareWith(): Promise<ShareWith> {
|
public async getRecommendedShareWith(shareType: ShareType[] = [OC.Share.SHARE_TYPE_USER, OC.Share.SHARE_TYPE_GROUP]): Promise<ShareWith> {
|
||||||
const url = OC.linkToOCS('apps/files_sharing/api/v1', 1) + 'sharees_recommended';
|
const url = OC.linkToOCS('apps/files_sharing/api/v1', 1) + 'sharees_recommended';
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
params: {
|
params: {
|
||||||
|
shareType,
|
||||||
itemType: 'room',
|
itemType: 'room',
|
||||||
format: 'json',
|
format: 'json',
|
||||||
},
|
},
|
||||||
|
@ -180,12 +231,12 @@ class Api {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async searchShareWith(search = ''): Promise<ShareWith> {
|
public async searchShareWith(search = '', shareType: ShareType[] = [OC.Share.SHARE_TYPE_USER, OC.Share.SHARE_TYPE_GROUP]): Promise<ShareWith> {
|
||||||
const url = OC.linkToOCS('apps/files_sharing/api/v1', 1) + 'sharees';
|
const url = OC.linkToOCS('apps/files_sharing/api/v1', 1) + 'sharees';
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
params: {
|
params: {
|
||||||
search,
|
search,
|
||||||
shareType: [OC.Share.SHARE_TYPE_USER, OC.Share.SHARE_TYPE_GROUP],
|
shareType,
|
||||||
itemType: 'room',
|
itemType: 'room',
|
||||||
format: 'json',
|
format: 'json',
|
||||||
lookup: false,
|
lookup: false,
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React, {useState} from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
values: string[];
|
||||||
|
setValue: (field: string, value: string[]) => Promise<void>;
|
||||||
|
field: string;
|
||||||
|
options: {[key: string]: string};
|
||||||
|
placeholder?: string;
|
||||||
|
invert?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditableSelection: React.FC<Props> = ({ setValue, field, values: currentValues, options, placeholder, invert = false }) => {
|
||||||
|
const [active, setActive] = useState<boolean>(false);
|
||||||
|
|
||||||
|
currentValues = currentValues || [];
|
||||||
|
|
||||||
|
function onClick(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
setActive(!active);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOption(optionKey: string, selected: boolean) {
|
||||||
|
if (selected) {
|
||||||
|
if (currentValues.indexOf(optionKey) < 0) {
|
||||||
|
setValue(field, [...currentValues, optionKey]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setValue(field, currentValues.filter(value => value !== optionKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = !invert ? currentValues : (currentValues.length ? Object.keys(options).filter(option => currentValues?.indexOf(option) < 0) : []);
|
||||||
|
|
||||||
|
return (<>
|
||||||
|
<a className="action-rename" onClick={onClick}>{selection.join(', ') || placeholder}</a>
|
||||||
|
{active && <ul className="bbb-selection">
|
||||||
|
{Object.keys(options).map(key => {
|
||||||
|
const label = options[key];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={key}
|
||||||
|
className="checkbox"
|
||||||
|
checked={(currentValues.indexOf(key) > -1) !== invert}
|
||||||
|
value="1"
|
||||||
|
onChange={(ev) => addOption(key, ev.target.checked !== invert)} />
|
||||||
|
<label htmlFor={key}>{label}</label>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>}
|
||||||
|
</>);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditableSelection;
|
|
@ -0,0 +1,33 @@
|
||||||
|
.bbb-selection-container {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bbb-selection {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid var(--color-border-dark);
|
||||||
|
max-height: 88px;
|
||||||
|
overflow: auto;
|
||||||
|
box-shadow: 0 5px 10px -5px var(--color-box-shadow);
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
li {
|
||||||
|
color: var(--color-text-lighter);
|
||||||
|
padding: 0 1em;
|
||||||
|
line-height: 44px;
|
||||||
|
|
||||||
|
&.suggestion {
|
||||||
|
color: var(--color-main-text);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { api, ShareWith, ShareType, ShareWithOption } from '../Common/Api';
|
||||||
|
import './ShareSelection.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectShare: (selection: ShareWithOption) => void;
|
||||||
|
shareType?: ShareType[];
|
||||||
|
excluded?: {
|
||||||
|
groupIds?: string[];
|
||||||
|
userIds?: string[];
|
||||||
|
};
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShareSelection: React.FC<Props> = (props) => {
|
||||||
|
const [search, setSearch] = useState<string>('');
|
||||||
|
const [hasFocus, setFocus] = useState<boolean>(false);
|
||||||
|
const [showSearchResults, setShowSearchResults] = useState<boolean>(false);
|
||||||
|
const [recommendations, setRecommendations] = useState<ShareWith>();
|
||||||
|
const [searchResults, setSearchResults] = useState<ShareWith>();
|
||||||
|
|
||||||
|
const shareType = props.shareType || [ShareType.User, ShareType.Group];
|
||||||
|
const excluded = {
|
||||||
|
userIds: props.excluded?.userIds || [],
|
||||||
|
groupIds: props.excluded?.groupIds || [],
|
||||||
|
};
|
||||||
|
const placeholder = props.placeholder || t('bbb', 'Name, group, ...');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchResults(undefined);
|
||||||
|
const searchQuery = search;
|
||||||
|
|
||||||
|
if (!searchQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.searchShareWith(searchQuery, shareType).then(result => {
|
||||||
|
if (searchQuery === search) {
|
||||||
|
setSearchResults(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.getRecommendedShareWith(shareType).then(result => setRecommendations(result));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => setShowSearchResults(hasFocus), 100);
|
||||||
|
}, [hasFocus]);
|
||||||
|
|
||||||
|
async function selectShare(share: ShareWithOption) {
|
||||||
|
props.selectShare(share);
|
||||||
|
|
||||||
|
setSearch('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSearchResults(options: ShareWith|undefined) {
|
||||||
|
const results = options ? [
|
||||||
|
...options.users.filter(user => !excluded.userIds.includes(user.value.shareWith)),
|
||||||
|
...options.groups.filter(group => !excluded.groupIds.includes(group.value.shareWith)),
|
||||||
|
] : [];
|
||||||
|
|
||||||
|
const renderOption = (option: ShareWithOption) => {
|
||||||
|
return (<li key={option.value.shareWith} className="suggestion" onClick={() => selectShare(option)}>
|
||||||
|
{option.label}{option.value.shareType === ShareType.Group ? ` (${t('bbb', 'Group')})` : ''}
|
||||||
|
</li>);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="bbb-selection">
|
||||||
|
{!options ?
|
||||||
|
<li><span className="icon icon-loading-small icon-visible"></span> {t('bbb', 'Searching')}</li> :
|
||||||
|
(
|
||||||
|
(results.length === 0 && search) ? <li>{t('bbb', 'No matches')}</li> : results.map(renderOption)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bbb-selection-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={search}
|
||||||
|
onChange={ev => setSearch(ev.currentTarget.value)}
|
||||||
|
onFocus={() => setFocus(true)}
|
||||||
|
onBlur={() => setFocus(false)}
|
||||||
|
placeholder={placeholder} />
|
||||||
|
{showSearchResults && renderSearchResults((search && searchResults) ? searchResults : ((recommendations && !search) ? recommendations : undefined))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareSelection;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Access } from './Api';
|
||||||
|
|
||||||
|
interface EscapeOptions {
|
||||||
|
escape?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bt(string: string, vars?: { [key: string]: string }, count?: number, options?: EscapeOptions): string {
|
||||||
|
return t('bbb', string, vars, count, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccessOptions = {
|
||||||
|
[Access.Public]: bt('Public'),
|
||||||
|
[Access.Password]: bt('Internal + Password protection for guests'),
|
||||||
|
[Access.WaitingRoom]: bt('Internal + Waiting room for guests'),
|
||||||
|
[Access.Internal]: bt('Internal'),
|
||||||
|
[Access.InternalRestricted]: bt('Internal restricted'),
|
||||||
|
};
|
|
@ -19,6 +19,10 @@
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
#bbb-root {
|
#bbb-root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -54,6 +58,10 @@
|
||||||
width: 250px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
@ -211,4 +219,4 @@
|
||||||
em {
|
em {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||||
import './App.scss';
|
import './App.scss';
|
||||||
import RoomRow from './RoomRow';
|
import RoomRow from './RoomRow';
|
||||||
import { SortArrow } from './SortArrow';
|
import { SortArrow } from './SortArrow';
|
||||||
import { api, Room } from './Api';
|
import { api, Room, Restriction, Access } from '../Common/Api';
|
||||||
import NewRoomForm from './NewRoomForm';
|
import NewRoomForm from './NewRoomForm';
|
||||||
|
|
||||||
export type SortKey = 'name' | 'welcome' | 'maxParticipants' | 'record';
|
export type SortKey = 'name' | 'welcome' | 'maxParticipants' | 'record';
|
||||||
|
@ -37,16 +37,19 @@ type Props = {
|
||||||
const App: React.FC<Props> = () => {
|
const App: React.FC<Props> = () => {
|
||||||
const [areRoomsLoaded, setRoomsLoaded] = useState(false);
|
const [areRoomsLoaded, setRoomsLoaded] = useState(false);
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
|
const [restriction, setRestriction] = useState<Restriction>();
|
||||||
const [rooms, setRooms] = useState<Room[]>([]);
|
const [rooms, setRooms] = useState<Room[]>([]);
|
||||||
const [orderBy, setOrderBy] = useState<SortKey>('name');
|
const [orderBy, setOrderBy] = useState<SortKey>('name');
|
||||||
const [sortOrder, setSortOrder] = useState(SortOrder.ASC);
|
const [sortOrder, setSortOrder] = useState(SortOrder.ASC);
|
||||||
|
|
||||||
const rows = rooms.sort(sortRooms(orderBy, sortOrder)).map(room => <RoomRow room={room} key={room.id} updateRoom={updateRoom} deleteRoom={deleteRoom} />);
|
const rows = rooms.sort(sortRooms(orderBy, sortOrder)).map(room => <RoomRow room={room} restriction={restriction} key={room.id} updateRoom={updateRoom} deleteRoom={deleteRoom} />);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (areRoomsLoaded) {
|
api.getRestriction().then(restriction => {
|
||||||
return;
|
setRestriction(restriction);
|
||||||
}
|
}).catch(err => {
|
||||||
|
console.warn('Could not load restriction', err);
|
||||||
|
});
|
||||||
|
|
||||||
api.getRooms().then(rooms => {
|
api.getRooms().then(rooms => {
|
||||||
setRooms(rooms);
|
setRooms(rooms);
|
||||||
|
@ -57,7 +60,7 @@ const App: React.FC<Props> = () => {
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
setRoomsLoaded(true);
|
setRoomsLoaded(true);
|
||||||
});
|
});
|
||||||
}, [areRoomsLoaded]);
|
}, []);
|
||||||
|
|
||||||
function onOrderBy(key: SortKey) {
|
function onOrderBy(key: SortKey) {
|
||||||
if (orderBy === key) {
|
if (orderBy === key) {
|
||||||
|
@ -72,7 +75,16 @@ const App: React.FC<Props> = () => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.createRoom(name).then(room => {
|
let access = Access.Public;
|
||||||
|
|
||||||
|
const disabledRoomTypes = restriction?.roomTypes || [];
|
||||||
|
if (disabledRoomTypes.length > 0 && disabledRoomTypes.indexOf(access) > -1) {
|
||||||
|
access = Object.values(Access).filter(a => disabledRoomTypes.indexOf(a) < 0)[0] as Access;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxParticipants = restriction?.maxParticipants || 0;
|
||||||
|
|
||||||
|
return api.createRoom(name, access, maxParticipants).then(room => {
|
||||||
setRooms(rooms.concat([room]));
|
setRooms(rooms.concat([room]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -95,6 +107,8 @@ const App: React.FC<Props> = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxRooms = restriction?.maxRooms || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="bbb-react-root"
|
<div id="bbb-react-root"
|
||||||
onClick={() => { /* @TODO hide edit inputs */ }}>
|
onClick={() => { /* @TODO hide edit inputs */ }}>
|
||||||
|
@ -131,9 +145,17 @@ const App: React.FC<Props> = () => {
|
||||||
{!areRoomsLoaded && <span className="icon icon-loading-small icon-visible"></span>}
|
{!areRoomsLoaded && <span className="icon icon-loading-small icon-visible"></span>}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<NewRoomForm addRoom={addRoom} />
|
{(maxRooms > rows.length || maxRooms < 0) ?
|
||||||
|
<NewRoomForm addRoom={addRoom} /> :
|
||||||
|
<p className="text-muted">{maxRooms === 0 ?
|
||||||
|
t('bbb', 'You are not permitted to create a room.') :
|
||||||
|
t('bbb', 'You exceeded the maximum number of rooms.')
|
||||||
|
}</p>}
|
||||||
|
</td>
|
||||||
|
<td />
|
||||||
|
<td colSpan={3}>
|
||||||
|
<p className="text-muted">{t('bbb', 'Room quota:')} {rooms.filter(room => room.userId === OC.currentUser).length} / {maxRooms}</p>
|
||||||
</td>
|
</td>
|
||||||
<td colSpan={4} />
|
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
|
@ -141,4 +163,4 @@ const App: React.FC<Props> = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Room } from './Api';
|
import { Room, Restriction } from '../Common/Api';
|
||||||
import EditRoomDialog from './EditRoomDialog';
|
import EditRoomDialog from './EditRoomDialog';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
room: Room;
|
room: Room;
|
||||||
updateProperty: (key: string, value: string | boolean | number) => Promise<void>;
|
restriction?: Restriction;
|
||||||
|
updateProperty: (key: string, value: string | boolean | number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditRoom: React.FC<Props> = ({ room, updateProperty }) => {
|
const EditRoom: React.FC<Props> = ({ room, restriction, updateProperty }) => {
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -16,9 +17,9 @@ const EditRoom: React.FC<Props> = ({ room, updateProperty }) => {
|
||||||
onClick={ev => { ev.preventDefault(), setOpen(true); }}
|
onClick={ev => { ev.preventDefault(), setOpen(true); }}
|
||||||
title={t('bbb', 'Edit')} />
|
title={t('bbb', 'Edit')} />
|
||||||
|
|
||||||
<EditRoomDialog room={room} updateProperty={updateProperty} open={open} setOpen={setOpen} />
|
<EditRoomDialog room={room} restriction={restriction} updateProperty={updateProperty} open={open} setOpen={setOpen} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditRoom;
|
export default EditRoom;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Access, Room, Permission, RoomShare, api } from './Api';
|
import { Access, Room, Permission, RoomShare, api, Restriction } from '../Common/Api';
|
||||||
import Dialog from './Dialog';
|
import Dialog from './Dialog';
|
||||||
import ShareWith from './ShareWith';
|
import ShareWith from './ShareWith';
|
||||||
import { SubmitInput } from './SubmitInput';
|
import { SubmitInput } from './SubmitInput';
|
||||||
|
import { AccessOptions } from '../Common/Translation';
|
||||||
|
|
||||||
const descriptions: { [key: string]: string } = {
|
const descriptions: { [key: string]: string } = {
|
||||||
name: t('bbb', 'Descriptive name of this room.'),
|
name: t('bbb', 'Descriptive name of this room.'),
|
||||||
|
@ -15,14 +16,18 @@ const descriptions: { [key: string]: string } = {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
restriction?: Restriction;
|
||||||
updateProperty: (key: string, value: string | boolean | number) => Promise<void>;
|
updateProperty: (key: string, value: string | boolean | number) => Promise<void>;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }) => {
|
const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, open, setOpen }) => {
|
||||||
const [shares, setShares] = useState<RoomShare[]>();
|
const [shares, setShares] = useState<RoomShare[]>();
|
||||||
|
|
||||||
|
const maxParticipantsLimit = (restriction?.maxParticipants || 0) < 0 ? undefined : restriction?.maxParticipants;
|
||||||
|
const minParticipantsLimit = (restriction?.maxParticipants || -1) < 1 ? 0 : 1;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return;
|
return;
|
||||||
|
@ -45,7 +50,7 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }
|
||||||
<h3>{label}</h3>
|
<h3>{label}</h3>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<SubmitInput initialValue={room[field]} type={type} name={field} onSubmitValue={value => updateProperty(field, value)} />
|
<SubmitInput initialValue={room[field]} type={type} name={field} onSubmitValue={value => updateProperty(field, value)} min={minParticipantsLimit} max={maxParticipantsLimit} />
|
||||||
{descriptions[field] && <em>{descriptions[field]}</em>}
|
{descriptions[field] && <em>{descriptions[field]}</em>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -71,19 +76,20 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessOptions = {...AccessOptions};
|
||||||
|
for(const roomType of restriction?.roomTypes || []) {
|
||||||
|
if (roomType !== room.access) {
|
||||||
|
delete accessOptions[roomType];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={() => setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}>
|
<Dialog open={open} onClose={() => setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}>
|
||||||
{inputElement(t('bbb', 'Name'), 'name')}
|
{inputElement(t('bbb', 'Name'), 'name')}
|
||||||
{inputElement(t('bbb', 'Welcome'), 'welcome')}
|
{inputElement(t('bbb', 'Welcome'), 'welcome')}
|
||||||
{inputElement(t('bbb', 'Participant limit'), 'maxParticipants', 'number')}
|
{inputElement(t('bbb', 'Participant limit'), 'maxParticipants', 'number')}
|
||||||
|
|
||||||
{selectElement(t('bbb', 'Access'), 'access', room.access, {
|
{selectElement(t('bbb', 'Access'), 'access', room.access, accessOptions, (value) => {
|
||||||
[Access.Public]: t('bbb', 'Public'),
|
|
||||||
[Access.Password]: t('bbb', 'Internal + Password protection for guests'),
|
|
||||||
[Access.WaitingRoom]: t('bbb', 'Internal + Waiting room for guests'),
|
|
||||||
[Access.Internal]: t('bbb', 'Internal'),
|
|
||||||
[Access.InternalRestricted]: t('bbb', 'Internal restricted'),
|
|
||||||
}, (value) => {
|
|
||||||
console.log('access', value);
|
console.log('access', value);
|
||||||
updateProperty('access', value);
|
updateProperty('access', value);
|
||||||
})}
|
})}
|
||||||
|
@ -117,6 +123,7 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="checkbox"
|
className="checkbox"
|
||||||
checked={room.record}
|
checked={room.record}
|
||||||
|
disabled={!restriction?.allowRecording}
|
||||||
onChange={(event) => updateProperty('record', event.target.checked)} />
|
onChange={(event) => updateProperty('record', event.target.checked)} />
|
||||||
<label htmlFor={`bbb-record-${room.id}`}>{t('bbb', 'Recording')}</label>
|
<label htmlFor={`bbb-record-${room.id}`}>{t('bbb', 'Recording')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,4 +133,4 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditRoomDialog;
|
export default EditRoomDialog;
|
||||||
|
|
|
@ -6,9 +6,14 @@ type EditableValueProps = {
|
||||||
setValue: (key: string, value: string | number) => Promise<void>;
|
setValue: (key: string, value: string | number) => Promise<void>;
|
||||||
field: string;
|
field: string;
|
||||||
type: 'text' | 'number';
|
type: 'text' | 'number';
|
||||||
|
options?: {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: currentValue, type }) => {
|
const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: currentValue, type, options }) => {
|
||||||
const [active, setActive] = useState<boolean>(false);
|
const [active, setActive] = useState<boolean>(false);
|
||||||
|
|
||||||
const submit = (value: string | number) => {
|
const submit = (value: string | number) => {
|
||||||
|
@ -31,6 +36,8 @@ const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: c
|
||||||
initialValue={currentValue}
|
initialValue={currentValue}
|
||||||
type={type}
|
type={type}
|
||||||
focus={true}
|
focus={true}
|
||||||
|
min={options?.min}
|
||||||
|
max={options?.max}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +47,11 @@ const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: c
|
||||||
setActive(true);
|
setActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.disabled) {
|
||||||
|
return <span>{currentValue}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
return <a className="action-rename" onClick={onClick}>{currentValue}</a>;
|
return <a className="action-rename" onClick={onClick}>{currentValue}</a>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditableValue;
|
export default EditableValue;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import { Recording } from './Api';
|
import { Recording } from '../Common/Api';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
recording: Recording;
|
recording: Recording;
|
||||||
|
@ -43,4 +43,4 @@ const RecordingRow: React.FC<Props> = ({recording, deleteRecording, storeRecordi
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RecordingRow;
|
export default RecordingRow;
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import { api, Recording, Room } from './Api';
|
import { api, Recording, Room, Restriction } from '../Common/Api';
|
||||||
import EditRoom from './EditRoom';
|
import EditRoom from './EditRoom';
|
||||||
import RecordingRow from './RecordingRow';
|
import RecordingRow from './RecordingRow';
|
||||||
import EditableValue from './EditableValue';
|
import EditableValue from './EditableValue';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
restriction?: Restriction;
|
||||||
updateRoom: (room: Room) => Promise<void>;
|
updateRoom: (room: Room) => Promise<void>;
|
||||||
deleteRoom: (id: number) => void;
|
deleteRoom: (id: number) => void;
|
||||||
}
|
}
|
||||||
|
@ -154,8 +155,8 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(field: string, type: 'text' | 'number' = 'text') {
|
function edit(field: string, type: 'text' | 'number' = 'text', options?) {
|
||||||
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} />;
|
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} options={options} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarUrl = OC.generateUrl('/avatar/' + encodeURIComponent(room.userId) + '/' + 24, {
|
const avatarUrl = OC.generateUrl('/avatar/' + encodeURIComponent(room.userId) + '/' + 24, {
|
||||||
|
@ -164,6 +165,9 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
requesttoken: OC.requestToken,
|
requesttoken: OC.requestToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const maxParticipantsLimit = props.restriction?.maxParticipants || -1;
|
||||||
|
const minParticipantsLimit = (props.restriction?.maxParticipants || -1) < 1 ? 0 : 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr className={showRecordings ? 'selected-row' : ''}>
|
<tr className={showRecordings ? 'selected-row' : ''}>
|
||||||
|
@ -185,15 +189,15 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
{room.userId !== OC.currentUser && <img src={avatarUrl} alt="Avatar" className="bbb-avatar" />}
|
{room.userId !== OC.currentUser && <img src={avatarUrl} alt="Avatar" className="bbb-avatar" />}
|
||||||
</td>
|
</td>
|
||||||
<td className="max-participants bbb-shrink">
|
<td className="max-participants bbb-shrink">
|
||||||
{edit('maxParticipants', 'number')}
|
{edit('maxParticipants', 'number', {min: minParticipantsLimit, max: maxParticipantsLimit < 0 ? undefined : maxParticipantsLimit})}
|
||||||
</td>
|
</td>
|
||||||
<td className="record bbb-shrink">
|
<td className="record bbb-shrink">
|
||||||
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" disabled={!props.restriction?.allowRecording} checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
||||||
<label htmlFor={`bbb-record-${room.id}`}></label>
|
<label htmlFor={`bbb-record-${room.id}`}></label>
|
||||||
</td>
|
</td>
|
||||||
<td className="bbb-shrink"><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
|
<td className="bbb-shrink"><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
|
||||||
<td className="edit icon-col">
|
<td className="edit icon-col">
|
||||||
<EditRoom room={props.room} updateProperty={updateRoom} />
|
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} />
|
||||||
</td>
|
</td>
|
||||||
<td className="remove icon-col">
|
<td className="remove icon-col">
|
||||||
<a className="icon icon-delete icon-visible"
|
<a className="icon icon-delete icon-visible"
|
||||||
|
|
|
@ -35,43 +35,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bbb-selection-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bbb-selection {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1px solid var(--color-border-dark);
|
|
||||||
max-height: 88px;
|
|
||||||
overflow: auto;
|
|
||||||
box-shadow: 0 5px 10px -5px var(--color-box-shadow);
|
|
||||||
|
|
||||||
li {
|
|
||||||
color: var(--color-text-lighter);
|
|
||||||
padding: 0 1em;
|
|
||||||
line-height: 44px;
|
|
||||||
|
|
||||||
&.suggestion {
|
|
||||||
color: var(--color-main-text);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bbb-form-shareWith {
|
|
||||||
margin-top: -1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bbb-icon-unselected {
|
.bbb-icon-unselected {
|
||||||
opacity: 0.2 !important;
|
opacity: 0.2 !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.5 !important;
|
opacity: 0.5 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { api, ShareWith, ShareType, RoomShare, Room, Permission, ShareWithOption } from './Api';
|
import { api, ShareWith, ShareType, RoomShare, Room, Permission } from '../Common/Api';
|
||||||
import './ShareWith.scss';
|
import './ShareWith.scss';
|
||||||
|
import ShareSelection from '../Common/ShareSelection';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -10,12 +11,6 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setShares }) => {
|
const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setShares }) => {
|
||||||
const [search, setSearch] = useState<string>('');
|
|
||||||
const [hasFocus, setFocus] = useState<boolean>(false);
|
|
||||||
const [showSearchResults, setShowSearchResults] = useState<boolean>(false);
|
|
||||||
const [recommendations, setRecommendations] = useState<ShareWith>();
|
|
||||||
const [searchResults, setSearchResults] = useState<ShareWith>();
|
|
||||||
|
|
||||||
const isOwner = room.userId === OC.currentUser;
|
const isOwner = room.userId === OC.currentUser;
|
||||||
|
|
||||||
const shares = (allShares && permission === Permission.Moderator) ?
|
const shares = (allShares && permission === Permission.Moderator) ?
|
||||||
|
@ -24,32 +19,11 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
const sharedUserIds = shares ? shares.filter(share => share.shareType === ShareType.User).map(share => share.shareWith) : [];
|
const sharedUserIds = shares ? shares.filter(share => share.shareType === ShareType.User).map(share => share.shareWith) : [];
|
||||||
const sharedGroupIds = shares ? shares.filter(share => share.shareType === ShareType.Group).map(share => share.shareWith) : [];
|
const sharedGroupIds = shares ? shares.filter(share => share.shareType === ShareType.Group).map(share => share.shareWith) : [];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSearchResults(undefined);
|
|
||||||
const searchQuery = search;
|
|
||||||
|
|
||||||
api.searchShareWith(searchQuery).then(result => {
|
|
||||||
if (searchQuery === search) {
|
|
||||||
setSearchResults(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [search]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
api.getRecommendedShareWith().then(result => setRecommendations(result));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => setShowSearchResults(hasFocus), 100);
|
|
||||||
}, [hasFocus]);
|
|
||||||
|
|
||||||
async function addRoomShare(shareWith: string, shareType: number, displayName: string, permission: Permission) {
|
async function addRoomShare(shareWith: string, shareType: number, displayName: string, permission: Permission) {
|
||||||
const roomShare = await api.createRoomShare(room.id, shareType, shareWith, permission);
|
const roomShare = await api.createRoomShare(room.id, shareType, shareWith, permission);
|
||||||
|
|
||||||
roomShare.shareWithDisplayName = displayName;
|
roomShare.shareWithDisplayName = displayName;
|
||||||
|
|
||||||
console.log('addRoomShare', allShares, roomShare);
|
|
||||||
|
|
||||||
const newShares = allShares ? [...allShares] : [];
|
const newShares = allShares ? [...allShares] : [];
|
||||||
const index = newShares.findIndex(share => share.id === roomShare.id);
|
const index = newShares.findIndex(share => share.id === roomShare.id);
|
||||||
|
|
||||||
|
@ -59,10 +33,7 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
newShares.push(roomShare);
|
newShares.push(roomShare);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('newroomshares', newShares);
|
|
||||||
|
|
||||||
setShares(newShares);
|
setShares(newShares);
|
||||||
setSearch('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRoomShare(id: number) {
|
async function deleteRoomShare(id: number) {
|
||||||
|
@ -79,27 +50,12 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
return addRoomShare(share.shareWith, share.shareType, share.shareWithDisplayName || share.shareWith, newPermission);
|
return addRoomShare(share.shareWith, share.shareType, share.shareWithDisplayName || share.shareWith, newPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSearchResults(options: ShareWith|undefined) {
|
function getAvatarUrl(userId: string) {
|
||||||
const results = options ? [
|
return OC.generateUrl('/avatar/' + encodeURIComponent(userId) + '/' + 32, {
|
||||||
...options.users.filter(user => !sharedUserIds.includes(user.value.shareWith)),
|
user: userId,
|
||||||
...options.groups.filter(group => !sharedGroupIds.includes(group.value.shareWith)),
|
size: 32,
|
||||||
] : [];
|
requesttoken: OC.requestToken,
|
||||||
|
});
|
||||||
const renderOption = (option: ShareWithOption) => {
|
|
||||||
return (<li key={option.value.shareWith} className="suggestion" onClick={() => addRoomShare(option.value.shareWith, option.value.shareType, option.label, permission)}>
|
|
||||||
{option.label}{option.value.shareType === ShareType.Group ? ` (${t('bbb', 'Group')})` : ''}
|
|
||||||
</li>);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className="bbb-selection">
|
|
||||||
{!options ?
|
|
||||||
<li><span className="icon icon-loading-small icon-visible"></span> {t('bbb', 'Searching')}</li> :
|
|
||||||
(
|
|
||||||
(results.length === 0 && search) ? <li>{t('bbb', 'No matches')}</li> : results.map(renderOption)
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderShares(shares: RoomShare[]) {
|
function renderShares(shares: RoomShare[]) {
|
||||||
|
@ -116,11 +72,7 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
return (
|
return (
|
||||||
<ul className="bbb-shareWith">
|
<ul className="bbb-shareWith">
|
||||||
{[ownShare, ...shares].map(share => {
|
{[ownShare, ...shares].map(share => {
|
||||||
const avatarUrl = share.shareType === ShareType.User ? OC.generateUrl('/avatar/' + encodeURIComponent(share.shareWith) + '/' + 32, {
|
const avatarUrl = share.shareType === ShareType.User ? getAvatarUrl(share.shareWith) : undefined;
|
||||||
user: share.shareWith,
|
|
||||||
size: 32,
|
|
||||||
requesttoken: OC.requestToken,
|
|
||||||
}) : undefined;
|
|
||||||
const displayName = share.shareWithDisplayName || share.shareWith;
|
const displayName = share.shareWithDisplayName || share.shareWith;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -136,7 +88,10 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
</div>
|
</div>
|
||||||
{(share.id > -1 && permission === Permission.Moderator && isOwner) && <div className="bbb-shareWith__item__action">
|
{(share.id > -1 && permission === Permission.Moderator && isOwner) && <div className="bbb-shareWith__item__action">
|
||||||
<a className={`icon icon-shared icon-visible ${share.permission === Permission.Admin ? 'bbb-icon-selected' : 'bbb-icon-unselected'}`}
|
<a className={`icon icon-shared icon-visible ${share.permission === Permission.Admin ? 'bbb-icon-selected' : 'bbb-icon-unselected'}`}
|
||||||
onClick={ev => {ev.preventDefault(); toggleAdminShare(share);}}
|
onClick={ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
toggleAdminShare(share);
|
||||||
|
}}
|
||||||
title={t('bbb', 'Share')} />
|
title={t('bbb', 'Share')} />
|
||||||
</div>}
|
</div>}
|
||||||
{(share.id > -1 && isOwner) && <div className="bbb-shareWith__item__action">
|
{(share.id > -1 && isOwner) && <div className="bbb-shareWith__item__action">
|
||||||
|
@ -157,19 +112,16 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
<>
|
<>
|
||||||
{shares ? renderShares(shares) : loading}
|
{shares ? renderShares(shares) : loading}
|
||||||
|
|
||||||
<div className="bbb-selection-container">
|
{isOwner ?
|
||||||
{isOwner ? <input
|
<ShareSelection
|
||||||
type="text"
|
selectShare={(shareOption) => addRoomShare(shareOption.value.shareWith, shareOption.value.shareType, shareOption.label, permission)}
|
||||||
value={search}
|
excluded={{userIds: sharedUserIds, groupIds: sharedGroupIds}}/> :
|
||||||
onChange={ev => setSearch(ev.currentTarget.value)}
|
<em>
|
||||||
onFocus={() => setFocus(true)}
|
<span className="icon icon-details icon-visible"></span> {t('bbb', 'You are not allowed to change this option, because this room is shared with you.')}
|
||||||
onBlur={() => setFocus(false)}
|
</em>
|
||||||
placeholder={t('bbb', 'Name, group, ...')} /> :
|
}
|
||||||
<em><span className="icon icon-details icon-visible"></span> {t('bbb', 'You are not allowed to change this option, because this room is shared with you.')}</em>}
|
|
||||||
{showSearchResults && renderSearchResults((search && searchResults) ? searchResults : ((recommendations && !search) ? recommendations : undefined))}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShareWith;
|
export default ShareWith;
|
||||||
|
|
|
@ -40,6 +40,8 @@ export class SubmitInput extends Component<SubmitInputProps, SubmitInputState> {
|
||||||
onChange={event => this.setState({value: event.currentTarget.value})}
|
onChange={event => this.setState({value: event.currentTarget.value})}
|
||||||
onBlur={() => this.props.onSubmitValue(this.state.value)}
|
onBlur={() => this.props.onSubmitValue(this.state.value)}
|
||||||
autoFocus={this.props.focus}
|
autoFocus={this.props.focus}
|
||||||
|
min={this.props.min}
|
||||||
|
max={this.props.max}
|
||||||
/>
|
/>
|
||||||
</form>;
|
</form>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import '../Manager/App.scss';
|
||||||
|
import { api, Restriction, ShareType } from '../Common/Api';
|
||||||
|
import RestrictionRow from './RestrictionRow';
|
||||||
|
import ShareSelection from '../Common/ShareSelection';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: React.FC<Props> = () => {
|
||||||
|
const [areRestrictionsLoaded, setRestrictionsLoaded] = useState(false);
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [restrictions, setRestrictions] = useState<Restriction[]>([]);
|
||||||
|
|
||||||
|
const rows = restrictions.sort((a, b) => a.groupId.localeCompare(b.groupId)).map(restriction => <RestrictionRow key={restriction.id} restriction={restriction} updateRestriction={updateRestriction} deleteRestriction={deleteRestriction} />);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.getRestrictions().then(restrictions => {
|
||||||
|
setRestrictions(restrictions);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn('Could not load restrictions', err);
|
||||||
|
|
||||||
|
setError(t('bbb', 'Server error'));
|
||||||
|
}).then(() => {
|
||||||
|
setRestrictionsLoaded(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function addRestriction(groupId: string) {
|
||||||
|
return api.createRestriction(groupId).then(restriction => {
|
||||||
|
setRestrictions([...restrictions, restriction]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRestriction(restriction: Restriction) {
|
||||||
|
return api.updateRestriction(restriction).then(updatedRestriction => {
|
||||||
|
setRestrictions(restrictions.map(restriction => {
|
||||||
|
if (restriction.id === updatedRestriction.id || restriction.groupId === updatedRestriction.groupId) {
|
||||||
|
return updatedRestriction;
|
||||||
|
}
|
||||||
|
|
||||||
|
return restriction;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRestriction(id: number) {
|
||||||
|
api.deleteRestriction(id).then(deletedRestriction => {
|
||||||
|
setRestrictions(restrictions.filter(restriction => restriction.id !== deletedRestriction.id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="bbb-react-root">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{t('bbb', 'Group name')}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{t('bbb', 'Max. rooms')}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{t('bbb', 'Access options')}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{t('bbb', 'Max. participants')}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{t('bbb', 'Recording')}
|
||||||
|
</th>
|
||||||
|
<th/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{!areRestrictionsLoaded
|
||||||
|
? <span className="icon icon-loading-small icon-visible"></span>
|
||||||
|
: <ShareSelection
|
||||||
|
placeholder={t('bbb', 'Group, ...')}
|
||||||
|
selectShare={(share) => addRestriction(share.value.shareWith)}
|
||||||
|
shareType={[ShareType.Group]}
|
||||||
|
excluded={{groupIds: restrictions.map(restriction => restriction.groupId)}} /> }
|
||||||
|
{error && <><span className="icon icon-error icon-visible"></span> {error}</>}
|
||||||
|
</td>
|
||||||
|
<td colSpan={4} />
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p className="text-muted">{t('bbb', 'Restrictions do not affect existing rooms. Minus one means the value is unlimited. The least restrictive option is chosen for every user if multiple restrictions apply.')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
|
@ -0,0 +1,83 @@
|
||||||
|
import React, { } from 'react';
|
||||||
|
import { Restriction } from '../Common/Api';
|
||||||
|
import EditableValue from '../Manager/EditableValue';
|
||||||
|
import EditableSelection from '../Common/EditableSelection';
|
||||||
|
import { AccessOptions } from '../Common/Translation';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
restriction: Restriction;
|
||||||
|
updateRestriction: (restriction: Restriction) => Promise<void>;
|
||||||
|
deleteRestriction: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const RestrictionRoom: React.FC<Props> = (props) => {
|
||||||
|
const restriction = props.restriction;
|
||||||
|
|
||||||
|
function updateRestriction(key: string, value: string | boolean | number | string[]) {
|
||||||
|
return props.updateRestriction({
|
||||||
|
...props.restriction,
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRow(ev: MouseEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
OC.dialogs.confirm(
|
||||||
|
t('bbb', 'Are you sure you want to delete the restrictions for group "{name}"? This operation can not be undone.', { name: restriction.groupId }),
|
||||||
|
t('bbb', 'Delete restrictions for "{name}"?', { name: restriction.groupId }),
|
||||||
|
confirmed => {
|
||||||
|
if (confirmed) {
|
||||||
|
props.deleteRestriction(restriction.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(field: string, type: 'text' | 'number' = 'text') {
|
||||||
|
return <EditableValue field={field} value={restriction[field]} setValue={updateRestriction} type={type} options={{min: -1}} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className="name">{restriction.groupId || t('bbb', 'All users')}</td>
|
||||||
|
<td className="max-rooms bbb-shrink">
|
||||||
|
{edit('maxRooms', 'number')}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<EditableSelection
|
||||||
|
field="roomTypes"
|
||||||
|
values={restriction.roomTypes}
|
||||||
|
options={AccessOptions}
|
||||||
|
setValue={updateRestriction}
|
||||||
|
invert={true}
|
||||||
|
placeholder={t('bbb', 'All')} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="max-participants bbb-shrink">
|
||||||
|
{edit('maxParticipants', 'number')}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="record bbb-shrink">
|
||||||
|
<input
|
||||||
|
id={`bbb-record-${restriction.id}`}
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={restriction.allowRecording}
|
||||||
|
onChange={(event) => updateRestriction('allowRecording', event.target.checked)} />
|
||||||
|
<label htmlFor={`bbb-record-${restriction.id}`}></label>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="remove icon-col">
|
||||||
|
{restriction.groupId && <a className="icon icon-delete icon-visible"
|
||||||
|
onClick={deleteRow as any}
|
||||||
|
title={t('bbb', 'Delete')} />}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RestrictionRoom;
|
|
@ -0,0 +1,12 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDom from 'react-dom';
|
||||||
|
|
||||||
|
// Enable React devtools
|
||||||
|
window['React'] = React;
|
||||||
|
|
||||||
|
$(document).ready(() => {
|
||||||
|
ReactDom.render( <App/>, document.getElementById('bbb-restrictions'));
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import {api} from './Manager/Api';
|
import {api} from './Common/Api';
|
||||||
import './Manager/App.scss';
|
import './Manager/App.scss';
|
||||||
|
|
||||||
declare const OCP: any;
|
declare const OCP: any;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import axios from '@nextcloud/axios';
|
import axios from '@nextcloud/axios';
|
||||||
import { generateOcsUrl, generateUrl } from '@nextcloud/router';
|
import { generateOcsUrl, generateUrl } from '@nextcloud/router';
|
||||||
import { Room } from './Manager/Api';
|
import { Room } from './Common/Api';
|
||||||
|
|
||||||
declare const OCA: any;
|
declare const OCA: any;
|
||||||
|
|
||||||
|
@ -75,4 +75,4 @@ $(() => {
|
||||||
|
|
||||||
return createResponse.data?.ocs?.data?.url;
|
return createResponse.data?.ocs?.data?.url;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,9 @@ module.exports = {
|
||||||
manager: [
|
manager: [
|
||||||
path.join(__dirname, 'ts', 'Manager', 'index.tsx'),
|
path.join(__dirname, 'ts', 'Manager', 'index.tsx'),
|
||||||
],
|
],
|
||||||
|
restrictions: [
|
||||||
|
path.join(__dirname, 'ts', 'Restrictions', 'index.tsx'),
|
||||||
|
],
|
||||||
join: [
|
join: [
|
||||||
path.join(__dirname, 'ts', 'join.ts'),
|
path.join(__dirname, 'ts', 'join.ts'),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue