chore: upgrade psalm, some deprecated, lint, use npm (not yarn), react sync with typescript

Signed-off-by: Sebastien Marinier <sebastien.marinier@arawa.fr>
pull/407/head
Sebastien Marinier 2025-12-11 17:21:26 +01:00
parent 6b6ad44753
commit 77b86b5820
49 changed files with 22053 additions and 7418 deletions

View File

@ -21,34 +21,34 @@ install-composer-deps-dev: composer.phar
php composer.phar install -o php composer.phar install -o
js-init: js-init:
yarn install npm install
yarn-update: npm-update:
yarn update npm update
# Building # Building
build-js: js-init build-js: js-init
yarn run dev npm run dev
build-js-production: js-init build-js-production: js-init
yarn run build npm run build
watch-js: js-init watch-js: js-init
yarn run watch npm run watch
# Linting # Linting
lint: js-init lint: js-init
yarn run lint npm run lint
lint-fix: js-init lint-fix: js-init
yarn run fix npm run fix
# Style linting # Style linting
stylelint: js-init stylelint: js-init
yarn run lint:style npm run lint:style
stylelint-fix: js-init stylelint-fix: js-init
yarn run lint:fix:style npm run lint:fix:style
phplint: phplint:
./vendor/bin/php-cs-fixer fix --dry-run ./vendor/bin/php-cs-fixer fix --dry-run

View File

@ -1,6 +1,3 @@
module.exports = { const babelConfig = require('@nextcloud/babel-config')
plugins: [
'@babel/plugin-syntax-dynamic-import', module.exports = babelConfig
],
presets: ['@babel/preset-env'],
}

View File

@ -17,7 +17,7 @@
"nextcloud/coding-standard": "^1.1.0", "nextcloud/coding-standard": "^1.1.0",
"phpstan/phpstan": "^2.1.16", "phpstan/phpstan": "^2.1.16",
"nextcloud/ocp": "^29.0 || ^30.0 || ^31.0", "nextcloud/ocp": "^29.0 || ^30.0 || ^31.0",
"vimeo/psalm": "5.9.0 || ^6.1.0", "vimeo/psalm": "^6.1.0",
"psr/container": "^1.1.2 || ^1.1.4 || ^2.0.2" "psr/container": "^1.1.2 || ^1.1.4 || ^2.0.2"
}, },
"config": { "config": {

2745
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ use \OCA\BigBlueButton\Middleware\HookMiddleware;
use \OCA\BigBlueButton\Middleware\JoinMiddleware; use \OCA\BigBlueButton\Middleware\JoinMiddleware;
use \OCA\BigBlueButton\Search\Provider; use \OCA\BigBlueButton\Search\Provider;
use \OCP\AppFramework\App; use \OCP\AppFramework\App;
use \OCP\IConfig; use \OCP\IAppConfig;
use \OCP\Settings\IManager as ISettingsManager; use \OCP\Settings\IManager as ISettingsManager;
use \OCP\User\Events\UserDeletedEvent; use \OCP\User\Events\UserDeletedEvent;
use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootContext;
@ -66,11 +66,11 @@ class Application extends App implements IBootstrap {
public function boot(IBootContext $context): void { public function boot(IBootContext $context): void {
$context->injectFn([$this, 'registerAdminPage']); $context->injectFn([$this, 'registerAdminPage']);
Util::addScript('bbb', 'filelist'); Util::addScript('bbb', 'bbb-filelist');
} }
public function registerAdminPage(ISettingsManager $settingsManager, INavigationManager $navigationManager, IURLGenerator $urlGenerator, IConfig $config):void { public function registerAdminPage(ISettingsManager $settingsManager, INavigationManager $navigationManager, IURLGenerator $urlGenerator, IAppConfig $config):void {
if ($config->getAppValue(self::ID, 'app.navigation') === 'true') { if ($config->getValueBool(self::ID, 'app.navigation')) {
$this->registerAsNavigationEntry($navigationManager, $urlGenerator, $config); $this->registerAsNavigationEntry($navigationManager, $urlGenerator, $config);
} else { } else {
$this->registerAsPersonalSetting($settingsManager); $this->registerAsPersonalSetting($settingsManager);
@ -78,11 +78,11 @@ class Application extends App implements IBootstrap {
} }
private function registerAsPersonalSetting(ISettingsManager $settingsManager): void { private function registerAsPersonalSetting(ISettingsManager $settingsManager): void {
$settingsManager->registerSetting(ISettingsManager::KEY_PERSONAL_SETTINGS, \OCA\BigBlueButton\Settings\Personal::class); $settingsManager->registerSetting(ISettingsManager::SETTINGS_PERSONAL, \OCA\BigBlueButton\Settings\Personal::class);
} }
private function registerAsNavigationEntry(INavigationManager $navigationManager, IURLGenerator $urlGenerator, IConfig $config): void { private function registerAsNavigationEntry(INavigationManager $navigationManager, IURLGenerator $urlGenerator, IAppConfig $config): void {
$name = $config->getAppValue(self::ID, 'app.navigation.name', 'BBB'); $name = $config->getValueString(self::ID, 'app.navigation.name', 'BBB');
$navigationManager->add(function () use ($urlGenerator, $name) { $navigationManager->add(function () use ($urlGenerator, $name) {
return [ return [

View File

@ -4,8 +4,8 @@ namespace OCA\BigBlueButton;
use OCA\BigBlueButton\AppInfo\Application; use OCA\BigBlueButton\AppInfo\Application;
use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\Room;
use OCP\IAppConfig;
use OCP\IAvatarManager; use OCP\IAvatarManager;
use OCP\IConfig;
use OCP\IURLGenerator; use OCP\IURLGenerator;
use OCP\Security\ISecureRandom; use OCP\Security\ISecureRandom;
@ -13,27 +13,11 @@ class AvatarRepository {
public const CONF_KEY_PATH = 'avatar.path'; public const CONF_KEY_PATH = 'avatar.path';
public const CONF_KEY_URL = 'avatar.url'; public const CONF_KEY_URL = 'avatar.url';
/** @var IAvatarManager */
private $avatarManager;
/** @var ISecureRandom */
private $random;
/** @var IURLGenerator */
private $urlGenerator;
/** @var IConfig */
private $config;
public function __construct( public function __construct(
IAvatarManager $avatarManager, private IAvatarManager $avatarManager,
IURLGenerator $urlGenerator, private IURLGenerator $urlGenerator,
ISecureRandom $random, private ISecureRandom $random,
IConfig $config) { private IAppConfig $config) {
$this->avatarManager = $avatarManager;
$this->urlGenerator = $urlGenerator;
$this->random = $random;
$this->config = $config;
} }
public function getAvatarUrl(Room $room, string $userId): string { public function getAvatarUrl(Room $room, string $userId): string {
@ -137,7 +121,7 @@ class AvatarRepository {
} }
private function getRootPath(): string { private function getRootPath(): string {
$path = $this->config->getAppValue(Application::ID, self::CONF_KEY_PATH); $path = $this->config->getValueString(Application::ID, self::CONF_KEY_PATH);
if (empty($path)) { if (empty($path)) {
return ''; return '';
@ -147,7 +131,7 @@ class AvatarRepository {
} }
private function getBaseUrl(): string { private function getBaseUrl(): string {
$url = $this->config->getAppValue(Application::ID, self::CONF_KEY_URL); $url = $this->config->getValueString(Application::ID, self::CONF_KEY_URL);
if (empty($url)) { if (empty($url)) {
return ''; return '';

View File

@ -20,73 +20,35 @@ use OCA\BigBlueButton\UrlHelper;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\Defaults; use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig; use OCP\IAppConfig;
use OCP\IL10N; use OCP\IL10N;
use OCP\IRequest; use OCP\IRequest;
use OCP\IURLGenerator; use OCP\IURLGenerator;
class API { class API {
/** @var IConfig */
private $config;
/** @var IURLGenerator */
private $urlGenerator;
/** @var BigBlueButton|null */ /** @var BigBlueButton|null */
private $server; private $server;
/** @var Crypto */
private $crypto;
/** @var IEventDispatcher */
private $eventDispatcher;
/** @var IL10N */
private $l10n;
/** @var UrlHelper */
private $urlHelper;
/** @var Defaults */
private $defaults;
/** @var IAppManager */
private $appManager;
/** @var AvatarRepository */
private $avatarRepository;
/** @var IRequest */
private $request;
public function __construct( public function __construct(
IConfig $config, private IAppConfig $config,
IURLGenerator $urlGenerator, private IURLGenerator $urlGenerator,
Crypto $crypto, private Crypto $crypto,
IEventDispatcher $eventDispatcher, private IEventDispatcher $eventDispatcher,
IL10N $l10n, private IL10N $l10n,
UrlHelper $urlHelper, private UrlHelper $urlHelper,
Defaults $defaults, private Defaults $defaults,
IAppManager $appManager, private IAppManager $appManager,
AvatarRepository $avatarRepository, private AvatarRepository $avatarRepository,
IRequest $request private IRequest $request
) { ) {
$this->config = $config; $this->server = null;
$this->urlGenerator = $urlGenerator;
$this->crypto = $crypto;
$this->eventDispatcher = $eventDispatcher;
$this->l10n = $l10n;
$this->urlHelper = $urlHelper;
$this->defaults = $defaults;
$this->appManager = $appManager;
$this->avatarRepository = $avatarRepository;
$this->request = $request;
} }
private function getServer(): BigBlueButton { private function getServer(): BigBlueButton {
if (!$this->server) { if (!$this->server) {
$apiUrl = $this->config->getAppValue('bbb', 'api.url'); $apiUrl = $this->config->getValueString('bbb', 'api.url');
$secret = $this->config->getAppValue('bbb', 'api.secret'); $secret = $this->config->getValueString('bbb', 'api.secret');
$this->server = new BigBlueButton($apiUrl, $secret); $this->server = new BigBlueButton($apiUrl, $secret);
} }
@ -123,7 +85,7 @@ class API {
$joinMeetingParams->addUserData('bbb_show_public_chat_on_login', false); $joinMeetingParams->addUserData('bbb_show_public_chat_on_login', false);
} }
if ($this->config->getAppValue('bbb', 'join.theme') === 'true') { if ($this->config->getValueBool('bbb', 'join.theme')) {
$primaryColor = $this->defaults->getColorPrimary(); $primaryColor = $this->defaults->getColorPrimary();
$textColor = $this->defaults->getTextColorPrimary(); $textColor = $this->defaults->getTextColorPrimary();
@ -160,7 +122,7 @@ class API {
} }
if ($response->getMessageKey() !== 'duplicateWarning') { if ($response->getMessageKey() !== 'duplicateWarning') {
$this->eventDispatcher->dispatch(MeetingStartedEvent::class, new MeetingStartedEvent($room)); $this->eventDispatcher->dispatchTyped(new MeetingStartedEvent($room));
} }
return $response->getCreationTime(); return $response->getCreationTime();
@ -179,7 +141,7 @@ class API {
$createMeetingParams->addMeta('bbb-origin', \method_exists($this->defaults, 'getProductName') ? $this->defaults->getProductName() : 'Nextcloud'); $createMeetingParams->addMeta('bbb-origin', \method_exists($this->defaults, 'getProductName') ? $this->defaults->getProductName() : 'Nextcloud');
$createMeetingParams->addMeta('bbb-origin-server-name', $this->request->getServerHost()); $createMeetingParams->addMeta('bbb-origin-server-name', $this->request->getServerHost());
$analyticsCallbackUrl = $this->config->getAppValue('bbb', 'api.meta_analytics-callback-url'); $analyticsCallbackUrl = $this->config->getValueString('bbb', 'api.meta_analytics-callback-url');
if (!empty($analyticsCallbackUrl)) { if (!empty($analyticsCallbackUrl)) {
// For more details: https://github.com/bigbluebutton/bigbluebutton/blob/develop/record-and-playback/core/scripts/post_events/post_events_analytics_callback.rb // For more details: https://github.com/bigbluebutton/bigbluebutton/blob/develop/record-and-playback/core/scripts/post_events/post_events_analytics_callback.rb
$createMeetingParams->addMeta('analytics-callback-url', $analyticsCallbackUrl); $createMeetingParams->addMeta('analytics-callback-url', $analyticsCallbackUrl);

View File

@ -50,7 +50,7 @@ class CircleHelper {
if ($this->api === null) { if ($this->api === null) {
if ($this->appManager->isEnabledForUser('circles') && class_exists('\OCA\Circles\Api\v1\Circles')) { if ($this->appManager->isEnabledForUser('circles') && class_exists('\OCA\Circles\Api\v1\Circles')) {
$container = $this->app->getContainer(); $container = $this->app->getContainer();
$this->api = $container->query(\OCA\Circles\Api\v1\Circles::class); $this->api = $container->get('OCA\Circles\Api\v1\Circles');
} else { } else {
$this->api = false; $this->api = false;
} }

View File

@ -25,7 +25,7 @@ class ClearAvatarCache extends Command {
$this->setDescription('Clear all avatars in cache'); $this->setDescription('Clear all avatars in cache');
} }
protected function execute(InputInterface $input, OutputInterface $output) { protected function execute(InputInterface $input, OutputInterface $output): int {
$stats = $this->avatarRepository->clearAllRooms(); $stats = $this->avatarRepository->clearAllRooms();
$output->writeln("Removed " . $stats["files"] . " avatars in " . $stats["rooms"] . " rooms"); $output->writeln("Removed " . $stats["files"] . " avatars in " . $stats["rooms"] . " rooms");

View File

@ -67,7 +67,7 @@ class HookController extends Controller {
$this->avatarRepository->clearRoom($room->uid); $this->avatarRepository->clearRoom($room->uid);
$this->eventDispatcher->dispatch(MeetingEndedEvent::class, new MeetingEndedEvent($room, $recordingmarks)); $this->eventDispatcher->dispatchTyped(new MeetingEndedEvent($room, $recordingmarks));
} }
/** /**
@ -78,7 +78,7 @@ class HookController extends Controller {
* @return void * @return void
*/ */
public function recordingReady(): void { public function recordingReady(): void {
$this->eventDispatcher->dispatch(RecordingReadyEvent::class, new RecordingReadyEvent($this->getRoom())); $this->eventDispatcher->dispatchTyped(new RecordingReadyEvent($this->getRoom()));
} }
private function getRoom(): ?Room { private function getRoom(): ?Room {

View File

@ -16,7 +16,7 @@ use OCP\AppFramework\Db\Entity;
* @method void setMaxRooms(int $number) * @method void setMaxRooms(int $number)
* @method void setMaxParticipants(int $number) * @method void setMaxParticipants(int $number)
* @method void setAllowRecording(bool $allow) * @method void setAllowRecording(bool $allow)
* @method void setGroupName(string $groupName) * @method void setGroupName(?string $groupName)
*/ */
class Restriction extends Entity implements JsonSerializable { class Restriction extends Entity implements JsonSerializable {
public const ALL_ID = ''; public const ALL_ID = '';

View File

@ -91,7 +91,7 @@ class RestrictionService {
return $this->mapper->insert($restriction); return $this->mapper->insert($restriction);
} }
public function update(int $id, string $groupId, int $maxRooms, array $roomTypes, int $maxParticipants, bool $allowRecording): Restriction { public function update(int $id, string $groupId, int $maxRooms, array $roomTypes, int $maxParticipants, bool $allowRecording): Restriction | null {
try { try {
$restriction = $this->mapper->find($id); $restriction = $this->mapper->find($id);
@ -104,10 +104,11 @@ class RestrictionService {
return $this->mapper->update($restriction); return $this->mapper->update($restriction);
} catch (Exception $e) { } catch (Exception $e) {
$this->handleException($e); $this->handleException($e);
return null;
} }
} }
public function delete(int $id): Restriction { public function delete(int $id): Restriction | null {
try { try {
$restriction = $this->mapper->find($id); $restriction = $this->mapper->find($id);
$this->mapper->delete($restriction); $this->mapper->delete($restriction);
@ -115,6 +116,7 @@ class RestrictionService {
return $restriction; return $restriction;
} catch (Exception $e) { } catch (Exception $e) {
$this->handleException($e); $this->handleException($e);
return null;
} }
} }

View File

@ -12,7 +12,7 @@ use OCA\BigBlueButton\Event\RoomDeletedEvent;
use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig; use OCP\IAppConfig;
use OCP\IUser; use OCP\IUser;
use OCP\Search\ISearchQuery; use OCP\Search\ISearchQuery;
use OCP\Security\ISecureRandom; use OCP\Security\ISecureRandom;
@ -21,7 +21,7 @@ class RoomService {
/** @var RoomMapper */ /** @var RoomMapper */
private $mapper; private $mapper;
/** @var IConfig */ /** @var IAppConfig */
private $config; private $config;
/** @var IEventDispatcher */ /** @var IEventDispatcher */
@ -32,7 +32,7 @@ class RoomService {
public function __construct( public function __construct(
RoomMapper $mapper, RoomMapper $mapper,
IConfig $config, IAppConfig $config,
IEventDispatcher $eventDispatcher, IEventDispatcher $eventDispatcher,
ISecureRandom $random) { ISecureRandom $random) {
$this->mapper = $mapper; $this->mapper = $mapper;
@ -96,7 +96,7 @@ class RoomService {
public function create(string $name, string $welcome, int $maxParticipants, bool $record, string $access, string $userId): \OCP\AppFramework\Db\Entity { public function create(string $name, string $welcome, int $maxParticipants, bool $record, string $access, string $userId): \OCP\AppFramework\Db\Entity {
$room = new Room(); $room = new Room();
$mediaCheck = $this->config->getAppValue('bbb', 'join.mediaCheck', 'true') === 'true'; $mediaCheck = $this->config->getValueBool('bbb', 'join.mediaCheck', true);
$room->setUid($this->humanReadableRandom(16)); $room->setUid($this->humanReadableRandom(16));
$room->setName($name); $room->setName($name);
@ -118,7 +118,7 @@ class RoomService {
$createdRoom = $this->mapper->insert($room); $createdRoom = $this->mapper->insert($room);
$this->eventDispatcher->dispatch(RoomCreatedEvent::class, new RoomCreatedEvent($createdRoom)); $this->eventDispatcher->dispatchTyped(new RoomCreatedEvent($createdRoom));
return $createdRoom; return $createdRoom;
} }
@ -195,7 +195,7 @@ class RoomService {
$this->mapper->delete($room); $this->mapper->delete($room);
$this->eventDispatcher->dispatch(RoomDeletedEvent::class, new RoomDeletedEvent($room)); $this->eventDispatcher->dispatchTyped(new RoomDeletedEvent($room));
return $room; return $room;
} catch (Exception $e) { } catch (Exception $e) {

View File

@ -52,7 +52,7 @@ class RoomShareService {
} }
} }
public function create(int $roomId, int $shareType, string $shareWith, int $permission): RoomShare { public function create(int $roomId, int $shareType, string $shareWith, int $permission): RoomShare | null {
try { try {
$roomShare = $this->mapper->findByRoomAndEntity($roomId, $shareWith, $shareType); $roomShare = $this->mapper->findByRoomAndEntity($roomId, $shareWith, $shareType);
@ -67,13 +67,13 @@ class RoomShareService {
$createdRoomShare = $this->mapper->insert($roomShare); $createdRoomShare = $this->mapper->insert($roomShare);
$this->eventDispatcher->dispatch(RoomShareCreatedEvent::class, new RoomShareCreatedEvent($createdRoomShare)); $this->eventDispatcher->dispatchTyped(new RoomShareCreatedEvent($createdRoomShare));
return $createdRoomShare; return $createdRoomShare;
} }
} }
public function update(int $id, int $roomId, int $shareType, string $shareWith, int $permission): RoomShare { public function update(int $id, int $roomId, int $shareType, string $shareWith, int $permission): RoomShare | null {
try { try {
$roomShare = $this->mapper->find($id); $roomShare = $this->mapper->find($id);
@ -85,19 +85,21 @@ class RoomShareService {
return $this->mapper->update($roomShare); return $this->mapper->update($roomShare);
} catch (Exception $e) { } catch (Exception $e) {
$this->handleException($e); $this->handleException($e);
return null;
} }
} }
public function delete(int $id): RoomShare { public function delete(int $id): RoomShare | null {
try { try {
$roomShare = $this->mapper->find($id); $roomShare = $this->mapper->find($id);
$this->mapper->delete($roomShare); $this->mapper->delete($roomShare);
$this->eventDispatcher->dispatch(RoomShareDeletedEvent::class, new RoomShareDeletedEvent($roomShare)); $this->eventDispatcher->dispatchTyped(new RoomShareDeletedEvent($roomShare));
return $roomShare; return $roomShare;
} catch (Exception $e) { } catch (Exception $e) {
$this->handleException($e); $this->handleException($e);
return null;
} }
} }
} }

View File

@ -3,20 +3,17 @@
namespace OCA\BigBlueButton\Settings; namespace OCA\BigBlueButton\Settings;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig; use OCP\IAppConfig;
use OCP\Settings\ISettings; use OCP\Settings\ISettings;
class Admin implements ISettings { class Admin implements ISettings {
/** @var IConfig */
private $config;
/** /**
* Admin constructor. * Admin constructor.
* *
* @param IConfig $config * @param IAppConfig $config
*/ */
public function __construct(IConfig $config) { public function __construct(private IAppConfig $config) {
$this->config = $config;
} }
/** /**
@ -24,12 +21,12 @@ class Admin implements ISettings {
*/ */
public function getForm() { public function getForm() {
$parameters = [ $parameters = [
'api.url' => $this->config->getAppValue('bbb', 'api.url'), 'api.url' => $this->config->getValueString('bbb', 'api.url'),
'api.secret' => $this->config->getAppValue('bbb', 'api.secret'), 'api.secret' => $this->config->getValueString('bbb', 'api.secret'),
'app.navigation' => $this->config->getAppValue('bbb', 'app.navigation') === 'true' ? 'checked' : '', 'app.navigation' => $this->config->getValueBool('bbb', 'app.navigation') ? 'checked' : '',
'join.theme' => $this->config->getAppValue('bbb', 'join.theme') === 'true' ? 'checked' : '', 'join.theme' => $this->config->getValueBool('bbb', 'join.theme') ? 'checked' : '',
'app.shortener' => $this->config->getAppValue('bbb', 'app.shortener'), 'app.shortener' => $this->config->getValueString('bbb', 'app.shortener'),
'join.mediaCheck' => $this->config->getAppValue('bbb', 'join.mediaCheck', 'true') === 'true' ? 'checked' : '', 'join.mediaCheck' => $this->config->getValueBool('bbb', 'join.mediaCheck', true) ? 'checked' : '',
]; ];
return new TemplateResponse('bbb', 'admin', $parameters); return new TemplateResponse('bbb', 'admin', $parameters);

View File

@ -3,24 +3,16 @@
namespace OCA\BigBlueButton; namespace OCA\BigBlueButton;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig; use OCP\IAppConfig;
use OCP\IL10N; use OCP\IL10N;
class TemplateProvider { class TemplateProvider {
/** @var IConfig */
private $config;
/** @var IL10N */
private $l;
/** /**
* Admin constructor. * Admin constructor.
* *
* @param IConfig $config
*/ */
public function __construct(IConfig $config, IL10N $l) { public function __construct(private IAppConfig $config, private IL10N $l) {
$this->config = $config;
$this->l = $l;
} }
/** /**
@ -29,13 +21,13 @@ class TemplateProvider {
public function getManager(): TemplateResponse { public function getManager(): TemplateResponse {
$warning = ''; $warning = '';
if (empty($this->config->getAppValue('bbb', 'api.url')) || empty($this->config->getAppValue('bbb', 'api.secret'))) { if (empty($this->config->getValueString('bbb', 'api.url')) || empty($this->config->getValueString('bbb', 'api.secret'))) {
$warning = $this->l->t('API URL or secret not configured. Please contact your administrator.'); $warning = $this->l->t('API URL or secret not configured. Please contact your administrator.');
} }
return new TemplateResponse('bbb', 'manager', [ return new TemplateResponse('bbb', 'manager', [
'warning' => $warning, 'warning' => $warning,
'shortener' => $this->config->getAppValue('bbb', 'app.shortener', ''), 'shortener' => $this->config->getValueString('bbb', 'app.shortener', ''),
]); ]);
} }
} }

View File

@ -3,26 +3,19 @@
namespace OCA\BigBlueButton; namespace OCA\BigBlueButton;
use OCA\BigBlueButton\Db\Room; use OCA\BigBlueButton\Db\Room;
use OCP\IConfig; use OCP\IAppConfig;
use OCP\IURLGenerator; use OCP\IURLGenerator;
class UrlHelper { class UrlHelper {
/** @var IConfig */
private $config;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct( public function __construct(
IConfig $config, private IAppConfig $config,
IURLGenerator $urlGenerator private IURLGenerator $urlGenerator
) { ) {
$this->config = $config;
$this->urlGenerator = $urlGenerator;
} }
public function linkToInvitationAbsolute(Room $room): string { public function linkToInvitationAbsolute(Room $room): string {
$url = $this->config->getAppValue('bbb', 'app.shortener', ''); $url = $this->config->getValueString('bbb', 'app.shortener', '');
if (empty($url) || strpos($url, 'https://') !== 0 || strpos($url, '{token}') === false) { if (empty($url) || strpos($url, 'https://') !== 0 || strpos($url, '{token}') === false) {
return $this->urlGenerator->linkToRouteAbsolute('bbb.join.index', ['token' => $room->getUid()]); return $this->urlGenerator->linkToRouteAbsolute('bbb.join.index', ['token' => $room->getUid()]);

19434
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -37,9 +37,10 @@
"@commitlint/cli": "^16.2.3", "@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^17.8.1", "@commitlint/config-conventional": "^17.8.1",
"@commitlint/travis-cli": "^16.2.3", "@commitlint/travis-cli": "^16.2.3",
"@nextcloud/axios": "^1.11.0", "@nextcloud/axios": "^2.5.1",
"@nextcloud/dialogs": "^3.1.2", "@nextcloud/dialogs": "^6.0.1",
"@nextcloud/router": "^2.0.0", "@nextcloud/files": "^3.12.0",
"@nextcloud/router": "^3.0.1",
"@octokit/rest": "^18.0.4", "@octokit/rest": "^18.0.4",
"archiver": "^5.0.0", "archiver": "^5.0.0",
"colors": "^1.4.0", "colors": "^1.4.0",
@ -55,8 +56,8 @@
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "yarn lint", "pre-commit": "npm run lint",
"pre-push": "yarn test:php:unit", "pre-push": "npm run test:php:unit",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
} }
}, },
@ -64,27 +65,30 @@
"extends @nextcloud/browserslist-config" "extends @nextcloud/browserslist-config"
], ],
"engines": { "engines": {
"node": ">=16.0.0" "node": "^20.0.0",
"npm": "^10.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.12.0",
"@babel/eslint-parser": "^7.27.1", "@babel/eslint-parser": "^7.27.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.9.0",
"@nextcloud/browserslist-config": "^2.2.0", "@nextcloud/babel-config": "^1.2.0",
"@nextcloud/browserslist-config": "^3.0.1",
"@nextcloud/eslint-plugin": "^2.0.0", "@nextcloud/eslint-plugin": "^2.0.0",
"@nextcloud/files": "^2.1.0", "@nextcloud/paths": "^2.3.0",
"@types/bootstrap": "^5.1.9", "@nextcloud/webpack-vue-config": "^5.5.1",
"@types/bootstrap": "^5.2.10",
"@types/inquirer": "^8.2.0", "@types/inquirer": "^8.2.0",
"@types/jquery": "^3.3.35", "@types/jquery": "^3.3.35",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/react": "^17.0.40", "@types/react": "^17.0.89",
"@types/webpack": "^5.28.0", "@types/react-dom": "^17.0.26",
"@types/webpack-env": "^1.15.2", "@types/react-transition-group": "^4.4.12",
"@types/webpack": "^5.28.5",
"@types/webpack-env": "^1.18.2",
"@typescript-eslint/eslint-plugin": "^5.15.0", "@typescript-eslint/eslint-plugin": "^5.15.0",
"@typescript-eslint/parser": "^5.15.0", "@typescript-eslint/parser": "^5.15.0",
"babel-loader": "^8.1.0",
"css-loader": "^6.7.1",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^8.11.0", "eslint": "^8.11.0",
"eslint-config-standard": "^17.0", "eslint-config-standard": "^17.0",
@ -92,7 +96,7 @@
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.6.0", "eslint-plugin-promise": "^6.6.0",
"eslint-plugin-react": "^7.19.0", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-standard": "^5.0.0", "eslint-plugin-standard": "^5.0.0",
"eslint-webpack-plugin": "^3.1.1", "eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
@ -100,12 +104,11 @@
"inquirer": "^8.2.6", "inquirer": "^8.2.6",
"install": "^0.13.0", "install": "^0.13.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"raw-loader": "^4.0.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-flip-move": "^3.0.4", "react-flip-move": "^3.0.4",
"react-hot-loader": "^4.12.20",
"react-select": "^5.2.2", "react-select": "^5.2.2",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"stylelint": "^14.5.3", "stylelint": "^14.5.3",
"stylelint-config-recommended-scss": "^5.0.2", "stylelint-config-recommended-scss": "^5.0.2",

View File

@ -24,7 +24,7 @@
</extraFiles> </extraFiles>
<issueHandlers> <issueHandlers>
<UndefinedClass> <UndefinedClass>
<errorLevel type="suppress"> <errorLevel type="info">
<referencedClass name="OC" /> <referencedClass name="OC" />
</errorLevel> </errorLevel>
</UndefinedClass> </UndefinedClass>
@ -36,5 +36,6 @@
<referencedClass name="Doctrine\DBAL\Schema\Table" /> <referencedClass name="Doctrine\DBAL\Schema\Table" />
</errorLevel> </errorLevel>
</UndefinedDocblockClass> </UndefinedDocblockClass>
<MissingOverrideAttribute errorLevel="suppress" />
</issueHandlers> </issueHandlers>
</psalm> </psalm>

View File

@ -2,8 +2,8 @@
/** @var $l \OCP\IL10N */ /** @var $l \OCP\IL10N */
/** @var $_ array */ /** @var $_ array */
script('bbb', 'admin'); \OCP\Util::addScript('bbb', 'bbb', 'admin');
script('bbb', 'restrictions'); \OCP\Util::addScript('bbb', 'bbb-restrictions');
?> ?>
<div id="bbb-settings" class="section"> <div id="bbb-settings" class="section">

View File

@ -2,7 +2,7 @@
/** @var $_ array */ /** @var $_ array */
/** @var $l \OCP\IL10N */ /** @var $l \OCP\IL10N */
style('core', 'guest'); style('core', 'guest');
script('bbb', 'join'); \OCP\Util::addScript('bbb', 'bbb-join');
?> ?>
<form method="get" action="?"> <form method="get" action="?">
<fieldset class="warning bbb"> <fieldset class="warning bbb">

View File

@ -1,5 +1,5 @@
<?php <?php
script('bbb', 'manager'); \OCP\Util::addScript('bbb', 'bbb-manager');
?> ?>
<div id="bbb-app"> <div id="bbb-app">

View File

@ -2,7 +2,7 @@
/** @var $_ array */ /** @var $_ array */
/** @var $l \OCP\IL10N */ /** @var $l \OCP\IL10N */
style('core', 'guest'); style('core', 'guest');
script('bbb', 'waiting'); \OCP\Util::addScript('bbb', 'bbb-waiting');
?> ?>
<div class="update bbb"> <div class="update bbb">

View File

@ -118,7 +118,7 @@ class Api {
public async updateRestriction(restriction: Restriction) { public async updateRestriction(restriction: Restriction) {
if (!restriction.id) { if (!restriction.id) {
const newRestriction = await this.createRestriction( const newRestriction = await this.createRestriction(
restriction.groupId restriction.groupId,
); );
restriction.id = newRestriction.id; restriction.id = newRestriction.id;
@ -166,7 +166,7 @@ class Api {
return response.data; return response.data;
} }
public async createRoom(name: string, access: Access = Access.Public, maxParticipants = 0) { public async createRoom(name: string, access: Access = Access.Public, maxParticipants = 0): Promise<Room> {
const response = await axios.post(this.getUrl('rooms'), { const response = await axios.post(this.getUrl('rooms'), {
name, name,
welcome: '', welcome: '',
@ -175,7 +175,7 @@ class Api {
access, access,
}); });
return response.data; return response.data as Room;
} }
public async updateRoom(room: Room) { public async updateRoom(room: Room) {
@ -202,7 +202,7 @@ class Api {
return response.data; return response.data;
} }
public async publishRecording(id: string, publish: boolean,) { public async publishRecording(id: string, publish: boolean) {
const response = await axios.post(this.getUrl(`server/record/${id}/publish`), { const response = await axios.post(this.getUrl(`server/record/${id}/publish`), {
published: publish, published: publish,
}); });

View File

@ -9,7 +9,7 @@ type Props = {
invert?: boolean; invert?: boolean;
} }
const EditableSelection: React.FC<Props> = ({ setValue, field, values: currentValues, options, placeholder, invert = false }) => { const EditableSelection = ({ setValue, field, values: currentValues, options, placeholder, invert = false }: Props): JSX.Element => {
const [active, setActive] = useState<boolean>(false); const [active, setActive] = useState<boolean>(false);
currentValues = currentValues || []; currentValues = currentValues || [];

View File

@ -13,7 +13,7 @@ type Props = {
placeholder?: string; placeholder?: string;
} }
const ShareSelection: React.FC<Props> = (props) => { const ShareSelection = (props: Props): JSX.Element => {
const [search, setSearch] = useState<string>(''); const [search, setSearch] = useState<string>('');
const [hasFocus, setFocus] = useState<boolean>(false); const [hasFocus, setFocus] = useState<boolean>(false);
const [showSearchResults, setShowSearchResults] = useState<boolean>(false); const [showSearchResults, setShowSearchResults] = useState<boolean>(false);

View File

@ -18,5 +18,5 @@ export const PermissionsOptions = {
}; };
export function html_sanitize_and_parse(str: string): string { export function html_sanitize_and_parse(str: string): string {
return parse(DOMPurify.sanitize(str, { USE_PROFILES: { html: true } })); return parse(DOMPurify.sanitize(str, { USE_PROFILES: { html: true } })) as string;
} }

View File

@ -31,11 +31,7 @@ function sortRooms(key: SortKey, orderBy: SortOrder) {
}; };
} }
type Props = { const App = () => {
}
const App: React.FC<Props> = () => {
const [isLoaded, setLoaded] = useState(false); const [isLoaded, setLoaded] = useState(false);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [restriction, setRestriction] = useState<Restriction>(); const [restriction, setRestriction] = useState<Restriction>();

View File

@ -4,9 +4,15 @@ type Props = {
open: boolean; open: boolean;
onClose?: () => void; onClose?: () => void;
title: string; title: string;
children: React.ReactNode;
} }
const Dialog: React.FC<Props> = ({open, title, children, onClose = () => undefined}) => { const Dialog = ({
open,
title,
children,
onClose = () => undefined,
}: Props): JSX.Element => {
if (!open) { if (!open) {
return <></>; return <></>;

View File

@ -8,7 +8,7 @@ type Props = {
updateProperty: (key: string, value: string | boolean | number | null) => Promise<void>; updateProperty: (key: string, value: string | boolean | number | null) => Promise<void>;
} }
const EditRoom: React.FC<Props> = ({ room, restriction, updateProperty }) => { const EditRoom = ({ room, restriction, updateProperty }: Props): JSX.Element => {
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
return ( return (

View File

@ -33,7 +33,7 @@ type Props = {
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
} }
const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, open, setOpen }) => { const EditRoomDialog = ({ room, restriction, updateProperty, open, setOpen }: Props): JSX.Element => {
const [shares, setShares] = useState<RoomShare[]>(); const [shares, setShares] = useState<RoomShare[]>();
const maxParticipantsLimit = (restriction?.maxParticipants || 0) < 0 ? undefined : restriction?.maxParticipants; const maxParticipantsLimit = (restriction?.maxParticipants || 0) < 0 ? undefined : restriction?.maxParticipants;
@ -69,7 +69,7 @@ const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, op
<h3>{label}</h3> <h3>{label}</h3>
</label> </label>
<SubmitInput initialValue={room[field]} type={type} name={field} onSubmitValue={value => updateProperty(field, value)} min={minParticipantsLimit} max={maxParticipantsLimit} /> <SubmitInput initialValue={room[field]} type={type} name={field} onSubmitValue={(value) => updateProperty(field, value)} min={minParticipantsLimit} max={maxParticipantsLimit} />
{descriptions[field] && <em>{html_sanitize_and_parse(descriptions[field])}</em>} {descriptions[field] && <em>{html_sanitize_and_parse(descriptions[field])}</em>}
</div> </div>
); );

View File

@ -13,7 +13,7 @@ type EditableValueProps = {
}; };
} }
const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: currentValue, type, options }) => { const EditableValue = ({ setValue, field, value: currentValue, type, options }: EditableValueProps): JSX.Element => {
const [active, setActive] = useState<boolean>(false); const [active, setActive] = useState<boolean>(false);
const submit = (value: string | number) => { const submit = (value: string | number) => {

View File

@ -4,7 +4,7 @@ type Props = {
addRoom: (name: string) => Promise<void>; addRoom: (name: string) => Promise<void>;
} }
const NewRoomForm: React.FC<Props> = (props) => { const NewRoomForm = (props: Props): JSX.Element => {
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>('');
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');

View File

@ -10,8 +10,7 @@ type Props = {
publishRecording: (recording: Recording, publish: boolean) => void; publishRecording: (recording: Recording, publish: boolean) => void;
} }
const RecordingRow: React.FC<Props> = ({recording, isAdmin, deleteRecording, storeRecording, publishRecording}) => { const RecordingRow = ({recording, isAdmin, deleteRecording, storeRecording, publishRecording}: Props): JSX.Element => {
function checkPublished(recording: Recording, onChange: (value: boolean) => void) { function checkPublished(recording: Recording, onChange: (value: boolean) => void) {
return ( return (
@ -26,7 +25,6 @@ const RecordingRow: React.FC<Props> = ({recording, isAdmin, deleteRecording, sto
); );
} }
return ( return (
<tr key={recording.id}> <tr key={recording.id}>
<td className="start icon-col"> <td className="start icon-col">

View File

@ -20,7 +20,7 @@ type RecordingsNumberProps = {
setShowRecordings: (showRecordings: boolean) => void; setShowRecordings: (showRecordings: boolean) => void;
} }
const RecordingsNumber: React.FC<RecordingsNumberProps> = ({ recordings, showRecordings, setShowRecordings }) => { const RecordingsNumber = ({ recordings, showRecordings, setShowRecordings }: RecordingsNumberProps): JSX.Element => {
if (recordings === null) { if (recordings === null) {
return <span className="icon icon-loading-small icon-visible"></span>; return <span className="icon icon-loading-small icon-visible"></span>;
} }
@ -36,7 +36,7 @@ const RecordingsNumber: React.FC<RecordingsNumberProps> = ({ recordings, showRec
return <span>0</span>; return <span>0</span>;
}; };
const RoomRow: React.FC<Props> = (props) => { const RoomRow = (props: Props): JSX.Element => {
const [recordings, setRecordings] = useState<Recording[] | null>(null); const [recordings, setRecordings] = useState<Recording[] | null>(null);
const [showRecordings, setShowRecordings] = useState<boolean>(false); const [showRecordings, setShowRecordings] = useState<boolean>(false);
const room = props.room; const room = props.room;
@ -74,7 +74,7 @@ const RoomRow: React.FC<Props> = (props) => {
props.deleteRoom(room.id); props.deleteRoom(room.id);
} }
}, },
true true,
); );
} }
@ -92,7 +92,7 @@ const RoomRow: React.FC<Props> = (props) => {
OC.dialogs.alert( OC.dialogs.alert(
t('bbb', 'URL to room could not be stored.'), t('bbb', 'URL to room could not be stored.'),
t('bbb', 'Error'), t('bbb', 'Error'),
() => undefined () => undefined,
); );
}); });
}, undefined, 'httpd/unix-directory'); }, undefined, 'httpd/unix-directory');
@ -112,7 +112,7 @@ const RoomRow: React.FC<Props> = (props) => {
OC.dialogs.alert( OC.dialogs.alert(
t('bbb', 'URL to presentation could not be stored.'), t('bbb', 'URL to presentation could not be stored.'),
t('bbb', 'Error'), t('bbb', 'Error'),
() => undefined () => undefined,
); );
}); });
}, undefined, 'httpd/unix-directory'); }, undefined, 'httpd/unix-directory');
@ -151,7 +151,7 @@ const RoomRow: React.FC<Props> = (props) => {
}); });
} }
}, },
true true,
); );
} }

View File

@ -11,7 +11,7 @@ type Props = {
setShares: (shares: RoomShare[]) => void; setShares: (shares: RoomShare[]) => void;
} }
const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setShares }) => { const ShareWith = ({ room, permission, shares: allShares, setShares }: Props): JSX.Element => {
const isOwner = room.userId === OC.currentUser; const isOwner = room.userId === OC.currentUser;
const shares = (allShares && permission === Permission.Moderator) ? const shares = (allShares && permission === Permission.Moderator) ?

View File

@ -1,7 +1,5 @@
import * as React from 'react'; import React, {
import { useState, useEffect, InputHTMLAttributes, SyntheticEvent,
Component, InputHTMLAttributes,
SyntheticEvent,
} from 'react'; } from 'react';
export interface SubmitInputProps extends InputHTMLAttributes<HTMLInputElement> { export interface SubmitInputProps extends InputHTMLAttributes<HTMLInputElement> {
@ -12,37 +10,41 @@ export interface SubmitInputProps extends InputHTMLAttributes<HTMLInputElement>
focus?: boolean; focus?: boolean;
} }
export interface SubmitInputState { export const SubmitInput = ({
value: string; type = 'text',
} initialValue = '',
name,
onSubmitValue,
focus,
min,
max,
...rest
}: SubmitInputProps): JSX.Element => {
const [value, setValue] = useState<string>(initialValue);
export class SubmitInput extends Component<SubmitInputProps, SubmitInputState> { useEffect(() => {
state: SubmitInputState = { setValue(initialValue ?? '');
value: '', }, [initialValue]);
const onSubmit = (e: SyntheticEvent) => {
e.preventDefault();
onSubmitValue(value);
}; };
constructor(props: SubmitInputProps) { return (
super(props); <form onSubmit={onSubmit}>
this.state.value = props.initialValue ?? ''; <input
} value={value}
type={type}
private onSubmit = (event: SyntheticEvent<any>) => { id={`bbb-${name}`}
event.preventDefault(); name={name}
this.props.onSubmitValue(this.state.value); onChange={(ev) => setValue((ev.target as HTMLInputElement).value)}
}; onBlur={() => onSubmitValue(value)}
autoFocus={focus}
public render(): JSX.Element { min={min}
return <form onSubmit={this.onSubmit}> max={max}
<input value={this.state.value} {...rest}
type={this.props.type}
id={`bbb-${this.props.name}`}
name={this.props.name}
onChange={event => this.setState({value: event.currentTarget.value})}
onBlur={() => this.props.onSubmitValue(this.state.value)}
autoFocus={this.props.focus}
min={this.props.min}
max={this.props.max}
/> />
</form>; </form>
} );
} };

View File

@ -8,5 +8,8 @@ import ReactDom from 'react-dom';
window['React'] = React; window['React'] = React;
$(document).ready(() => { $(document).ready(() => {
ReactDom.render( <App/>, document.getElementById('bbb-root')); const root = document.getElementById('bbb-root');
if (root) {
ReactDom.render( <App /> as any , root);
}
}); });

4
ts/Nextcloud.d.ts vendored
View File

@ -113,3 +113,7 @@ declare module 'NC' {
}; };
} }
} }
declare const OC: any;
declare const OCP: any;
declare const OCA: any;

View File

@ -4,16 +4,12 @@ import { api, Restriction, ShareType } from '../Common/Api';
import RestrictionRow from './RestrictionRow'; import RestrictionRow from './RestrictionRow';
import ShareSelection from '../Common/ShareSelection'; import ShareSelection from '../Common/ShareSelection';
type Props = { const App = (): JSX.Element => {
}
const App: React.FC<Props> = () => {
const [areRestrictionsLoaded, setRestrictionsLoaded] = useState(false); const [areRestrictionsLoaded, setRestrictionsLoaded] = useState(false);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [restrictions, setRestrictions] = useState<Restriction[]>([]); 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} />); const rows = restrictions.sort((a: Restriction, b: Restriction) => a.groupId.localeCompare(b.groupId)).map(restriction => <RestrictionRow key={restriction.id} restriction={restriction} updateRestriction={updateRestriction} deleteRestriction={deleteRestriction} />);
useEffect(() => { useEffect(() => {
api.getRestrictions().then(restrictions => { api.getRestrictions().then(restrictions => {
@ -35,7 +31,7 @@ const App: React.FC<Props> = () => {
function updateRestriction(restriction: Restriction) { function updateRestriction(restriction: Restriction) {
return api.updateRestriction(restriction).then(updatedRestriction => { return api.updateRestriction(restriction).then(updatedRestriction => {
setRestrictions(restrictions.map(restriction => { setRestrictions(restrictions.map((restriction: Restriction) => {
if (restriction.id === updatedRestriction.id || restriction.groupId === updatedRestriction.groupId) { if (restriction.id === updatedRestriction.id || restriction.groupId === updatedRestriction.groupId) {
return updatedRestriction; return updatedRestriction;
} }
@ -86,7 +82,7 @@ const App: React.FC<Props> = () => {
placeholder={t('bbb', 'Group …')} placeholder={t('bbb', 'Group …')}
selectShare={(share) => addRestriction(share.value.shareWith)} selectShare={(share) => addRestriction(share.value.shareWith)}
shareType={[ShareType.Group]} shareType={[ShareType.Group]}
excluded={{groupIds: restrictions.map(restriction => restriction.groupId)}} /> } excluded={{groupIds: restrictions.map((restriction: Restriction) => restriction.groupId)}} /> }
{error && <><span className="icon icon-error icon-visible"></span> {error}</>} {error && <><span className="icon icon-error icon-visible"></span> {error}</>}
</td> </td>
<td colSpan={4} /> <td colSpan={4} />

View File

@ -1,4 +1,4 @@
import React, { } from 'react'; import React from 'react';
import { Restriction } from '../Common/Api'; import { Restriction } from '../Common/Api';
import EditableValue from '../Manager/EditableValue'; import EditableValue from '../Manager/EditableValue';
import EditableSelection from '../Common/EditableSelection'; import EditableSelection from '../Common/EditableSelection';
@ -11,7 +11,7 @@ type Props = {
} }
const RestrictionRoom: React.FC<Props> = (props) => { const RestrictionRoom = (props: Props): JSX.Element => {
const restriction = props.restriction; const restriction = props.restriction;
function updateRestriction(key: string, value: string | boolean | number | string[]) { function updateRestriction(key: string, value: string | boolean | number | string[]) {
@ -32,7 +32,7 @@ const RestrictionRoom: React.FC<Props> = (props) => {
props.deleteRestriction(restriction.id); props.deleteRestriction(restriction.id);
} }
}, },
true true,
); );
} }

View File

@ -2,11 +2,14 @@
import App from './App'; import App from './App';
import React from 'react'; import React from 'react';
import ReactDom from 'react-dom'; import { render } from 'react-dom';
// Enable React devtools // Enable React devtools
window['React'] = React; window['React'] = React;
$(document).ready(() => { $(document).ready(() => {
ReactDom.render( <App/>, document.getElementById('bbb-restrictions')); const root = document.getElementById('bbb-restrictions');
if (root) {
render(<App /> as any, root);
}
}); });

View File

@ -1,8 +1,6 @@
import { api } from './Common/Api'; import { api } from './Common/Api';
import './Manager/App.scss'; import './Manager/App.scss';
declare const OCP: any;
$(() => { $(() => {
function generateWarningElement(message: string) { function generateWarningElement(message: string) {
return $(`<div id="bbb-warning"><span class="icon icon-error-color icon-visible"></span> ${message}</div>`); return $(`<div id="bbb-warning"><span class="icon icon-error-color icon-visible"></span> ${message}</div>`);
@ -23,14 +21,10 @@ $(() => {
} }
function checkPasswordConfirmation() { function checkPasswordConfirmation() {
return new Promise<void>(resolve => { return new Promise<void>((resolve: () => void) => {
if (OC.PasswordConfirmation && OC.PasswordConfirmation.requiresPasswordConfirmation()) { OC.PasswordConfirmation?.requiresPasswordConfirmation()
OC.PasswordConfirmation.requirePasswordConfirmation(() => resolve()); ? OC.PasswordConfirmation.requirePasswordConfirmation(() => resolve())
: resolve();
return;
}
resolve();
}); });
} }
@ -49,7 +43,14 @@ $(() => {
const resultElement = $(this).find('.bbb-result').empty(); const resultElement = $(this).find('.bbb-result').empty();
saveApiSettings(this['api.url'].value, this['api.secret'].value).then(() => { const apiUrl = this['api.url'] as HTMLInputElement;
const apiSecret = this['api.secret'] as HTMLInputElement;
if (apiUrl === null || apiSecret == null) {
return;
}
saveApiSettings(apiUrl.value, apiSecret.value).then(() => {
const successElement = generateSuccessElement(t('bbb', 'Settings saved')); const successElement = generateSuccessElement(t('bbb', 'Settings saved'));
setTimeout(() => { setTimeout(() => {
@ -95,7 +96,11 @@ $(() => {
const resultElement = $(this).find('.bbb-result').empty(); const resultElement = $(this).find('.bbb-result').empty();
saveAppSettings(this['app.shortener'].value).then(() => { const shortenerInput = this['app.shortener'] as HTMLInputElement;
if (shortenerInput === null) {
return;
}
saveAppSettings(shortenerInput.value).then(() => {
const successElement = generateSuccessElement(t('bbb', 'Settings saved')); const successElement = generateSuccessElement(t('bbb', 'Settings saved'));
setTimeout(() => { setTimeout(() => {
@ -123,7 +128,7 @@ $(() => {
$<HTMLInputElement>('#bbb-shortener [name="app.shortener"]').on('keyup', (ev) => { $<HTMLInputElement>('#bbb-shortener [name="app.shortener"]').on('keyup', (ev) => {
ev.preventDefault(); ev.preventDefault();
const {value} = ev.target; const {value} = ev.target as HTMLInputElement;
if (!value || value.indexOf('https://') !== 0 || value.indexOf('{token}') < 0) { if (!value || value.indexOf('https://') !== 0 || value.indexOf('{token}') < 0) {
$('#bbb-shortener-example').text(t('bbb', 'URL has to start with https:// and contain {token}. Additionally the {user} placeholder can be used.')); $('#bbb-shortener-example').text(t('bbb', 'URL has to start with https:// and contain {token}. Additionally the {user} placeholder can be used.'));
@ -154,8 +159,10 @@ return 307;</pre></details>
$<HTMLInputElement>('.bbb-setting[type="checkbox"]').on('change', (ev) => { $<HTMLInputElement>('.bbb-setting[type="checkbox"]').on('change', (ev) => {
ev.preventDefault(); ev.preventDefault();
console.log(`checkbox ${ev.target.name} changed to ${ev.target.checked}`); const inputElement = ev.target as HTMLInputElement;
OCP.AppConfig.setValue('bbb', ev.target.name, ev.target.checked); console.log(`checkbox ${inputElement.name} changed to ${inputElement.checked}`);
OCP.AppConfig.setValue('bbb', inputElement.name, inputElement.checked);
}); });
}); });

View File

@ -1,7 +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 { showSuccess, showWarning, showError } from '@nextcloud/dialogs'; import { showSuccess, showWarning, showError } from '@nextcloud/dialogs';
import '@nextcloud/dialogs/styles/toast';
import { api } from './Common/Api'; import { api } from './Common/Api';
import './filelist.scss'; import './filelist.scss';
@ -80,7 +79,9 @@ async function openDialog(fileId: number, filename: string) {
const initContent = '<div id="bbb-file-action"><span className="icon icon-loading-small icon-visible"></span></div>'; const initContent = '<div id="bbb-file-action"><span className="icon icon-loading-small icon-visible"></span></div>';
const title = t('bbb', 'Send file to BBB'); const title = t('bbb', 'Send file to BBB');
await (OC.dialogs as ExtendedDialogs).message(initContent, title, 'none', -1, undefined, true, true); const exDialogs = OC.dialogs as ExtendedDialogs;
await exDialogs.message(initContent, title, 'none', -1, undefined, true, true);
const rooms = await api.getRooms(); const rooms = await api.getRooms();

View File

@ -9,8 +9,8 @@ $(() => {
'bbb', 'bbb',
'This room is not open yet. We will try it again in %n second. Please wait.', 'This room is not open yet. We will try it again in %n second. Please wait.',
'This room is not open yet. We will try it again in %n seconds. Please wait.', 'This room is not open yet. We will try it again in %n seconds. Please wait.',
--countdown --countdown,
) ),
); );
if (countdown === 0) { if (countdown === 0) {

View File

@ -3,7 +3,7 @@
"lib": [ "lib": [
"dom", "dom",
"es2015.promise", "es2015.promise",
"es6" "es2017"
], ],
"module": "ES6", "module": "ES6",
"moduleResolution": "node", "moduleResolution": "node",

View File

@ -1,9 +1,30 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
process.env.npm_package_name = 'bbb';
const path = require('path'); const path = require('path');
const ESLintPlugin = require('eslint-webpack-plugin'); const ESLintPlugin = require('eslint-webpack-plugin');
const webpackConfig = require('@nextcloud/webpack-vue-config')
const webpackRules = require('@nextcloud/webpack-vue-config/rules')
module.exports = { webpackRules.RULE_TSX = {
entry: { test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
babelrc: false,
},
},
'ts-loader',
],
};
webpackRules.RULE_RAW = {
test: /\.svg$/,
resourceQuery: /raw/,
type: 'asset/source'
};
webpackConfig.entry = {
admin: [ admin: [
path.join(__dirname, 'ts', 'admin.ts'), path.join(__dirname, 'ts', 'admin.ts'),
], ],
@ -22,55 +43,12 @@ module.exports = {
waiting: [ waiting: [
path.join(__dirname, 'ts', 'waiting.ts'), path.join(__dirname, 'ts', 'waiting.ts'),
], ],
},
output: {
path: path.resolve(__dirname, './js'),
publicPath: '/js/',
filename: '[name].js',
chunkFilename: 'chunks/[name]-[hash].js',
},
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
babelrc: false,
plugins: ['react-hot-loader/babel'],
},
},
'ts-loader',
],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
generator: {
filename: 'static/[name][ext]?[hash]',
},
},
],
},
plugins: [
new ESLintPlugin(),
],
resolve: {
extensions: ['*', '.tsx', '.ts', '.js', '.scss'],
symlinks: false,
},
}; };
webpackConfig.module.rules = Object.values(webpackRules);
webpackConfig.plugins.push(new ESLintPlugin());
webpackConfig.resolve.extensions = [...webpackConfig.resolve.extensions, '.jsx', '.ts', '.tsx'];
module.exports = webpackConfig

6695
yarn.lock

File diff suppressed because it is too large Load Diff