From 1460ee5307ad6b1dc4cd2d09a82e8bdc542f2515 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Tue, 10 Oct 2023 07:50:41 +0000 Subject: [PATCH] introduce interface AdminCourseWidgetPlugin and adjust core admin courses to it, fixes #3084 Closes #3084 Merge request studip/studip!2071 --- app/controllers/admin/courses.php | 137 +++++++++------- ...g_configurations_used_in_admin_courses.php | 73 +++++++++ lib/classes/Widget.php | 18 ++- lib/classes/helpbar/HelpbarWidget.php | 27 +--- .../sidebar/AdminCourseOptionsWidget.php | 153 ++++++++++++++++++ .../sidebar/AttributesArrayAccessTrait.php | 44 +++++ lib/classes/sidebar/ButtonElement.php | 26 +++ lib/classes/sidebar/LinkElement.php | 44 +---- lib/classes/sidebar/OptionsWidget.php | 117 +++++++------- lib/classes/sidebar/SelectListElement.php | 55 +++++++ .../core/AdminCourseWidgetPlugin.class.php | 42 +++++ lib/plugins/engine/PluginEngine.class.php | 5 +- .../javascripts/bootstrap/admin-courses.js | 16 ++ resources/vue/store/AdminCoursesStore.js | 2 +- templates/sidebar/list-widget.php | 9 +- 15 files changed, 571 insertions(+), 197 deletions(-) create mode 100644 db/migrations/5.4.15_add_missing_configurations_used_in_admin_courses.php create mode 100644 lib/classes/sidebar/AdminCourseOptionsWidget.php create mode 100644 lib/classes/sidebar/AttributesArrayAccessTrait.php create mode 100644 lib/classes/sidebar/ButtonElement.php create mode 100644 lib/classes/sidebar/SelectListElement.php create mode 100644 lib/plugins/core/AdminCourseWidgetPlugin.class.php diff --git a/app/controllers/admin/courses.php b/app/controllers/admin/courses.php index b3c7ec82a63..d9eced342b0 100644 --- a/app/controllers/admin/courses.php +++ b/app/controllers/admin/courses.php @@ -235,6 +235,17 @@ class Admin_CoursesController extends AuthenticatedController )->asDialog('size=auto'); $sidebar->addWidget($export); } + + foreach (PluginEngine::getPlugins(AdminCourseWidgetPlugin::class) as $plugin) { + foreach ($plugin->getWidgets() as $name => $widget) { + $position = $widget->getPositionInSidebar(); + if ($position) { + $sidebar->insertWidget($widget, $position, $name); + } else { + $sidebar->addWidget($widget, $name); + } + } + } } @@ -315,21 +326,23 @@ class Admin_CoursesController extends AuthenticatedController ? $configuration->MY_INSTITUTES_DEFAULT : null; + $filters = array_merge( + array_merge(...PluginEngine::sendMessage(AdminCourseWidgetPlugin::class, 'getFilters')), + $this->getDatafieldFilters(), + [ + 'institut_id' => $institut_id, + 'search' => $configuration->ADMIN_COURSES_SEARCHTEXT, + 'semester_id' => $configuration->MY_COURSES_SELECTED_CYCLE, + 'course_type' => $configuration->MY_COURSES_TYPE_FILTER, + 'stgteil' => $configuration->MY_COURSES_SELECTED_STGTEIL, + 'teacher_filter' => $configuration->ADMIN_COURSES_TEACHERFILTER, + ] + ); return [ 'setActivatedFields' => $this->getFilterConfig(), 'setActionArea' => $configuration->MY_COURSES_ACTION_AREA ?? '1', - 'setFilter' => array_filter(array_merge( - $this->getDatafieldFilters(), - [ - 'institut_id' => $institut_id, - 'search' => $configuration->ADMIN_COURSES_SEARCHTEXT, - 'semester_id' => $configuration->MY_COURSES_SELECTED_CYCLE, - 'course_type' => $configuration->MY_COURSES_TYPE_FILTER, - 'stgteil' => $configuration->MY_COURSES_SELECTED_STGTEIL, - 'teacher_filter' => $configuration->ADMIN_COURSES_TEACHERFILTER, - ] - )), + 'setFilter' => array_filter($filters), ]; } @@ -357,59 +370,14 @@ class Admin_CoursesController extends AuthenticatedController public function search_action() { - $activeSidebarElements = $this->getActiveElements(); - if (Request::get('search')) { - $GLOBALS['user']->cfg->store('ADMIN_COURSES_SEARCHTEXT', Request::get('search')); - } else { - $GLOBALS['user']->cfg->delete('ADMIN_COURSES_SEARCHTEXT'); - } - if (Request::option('institut_id') && Request::option('institut_id') !== 'all') { - $GLOBALS['user']->cfg->store('MY_INSTITUTES_DEFAULT', Request::option('institut_id')); - } else { - $GLOBALS['user']->cfg->delete('MY_INSTITUTES_DEFAULT'); - } - - if (Request::option('semester_id')) { - $GLOBALS['user']->cfg->store('MY_COURSES_SELECTED_CYCLE', Request::option('semester_id')); - } else { - $GLOBALS['user']->cfg->delete('MY_COURSES_SELECTED_CYCLE'); - } - - if (Request::option('course_type') && Request::option('course_type') !== 'all') { - $GLOBALS['user']->cfg->store('MY_COURSES_TYPE_FILTER', Request::option('course_type')); - } else { - $GLOBALS['user']->cfg->delete('MY_COURSES_TYPE_FILTER'); - } - - if (Request::option('stgteil')) { - $GLOBALS['user']->cfg->store('MY_COURSES_SELECTED_STGTEIL', Request::option('stgteil')); - } else { - $GLOBALS['user']->cfg->delete('MY_COURSES_SELECTED_STGTEIL'); - } - - if (Request::option('teacher_filter')) { - $GLOBALS['user']->cfg->store('ADMIN_COURSES_TEACHERFILTER', Request::option('teacher_filter')); - } else { - $GLOBALS['user']->cfg->delete('ADMIN_COURSES_TEACHERFILTER'); - } - - $datafields_filters = $GLOBALS['user']->cfg->ADMIN_COURSES_DATAFIELDS_FILTERS; - foreach (DataField::getDataFields('sem') as $datafield) { - if ( - Request::get('df_'.$datafield->getId()) - && in_array($datafield->getId(), $activeSidebarElements['datafields']) - ) { - $datafields_filters[$datafield->getId()] = Request::get('df_'.$datafield->getId()); - } else { - unset($datafields_filters[$datafield->getId()]); - } - } - $GLOBALS['user']->cfg->store('ADMIN_COURSES_DATAFIELDS_FILTERS', $datafields_filters); + $this->processFilters(); $filter = AdminCourseFilter::get(); if (Request::option('course_id')) { //we have only one course and want to see if that course is part of the result set $filter->query->where('course_id', 'seminare.Seminar_id = :course_id', ['course_id' => Request::option('course_id')]); } + PluginEngine::sendMessage(AdminCourseWidgetPlugin::class, 'applyFilters', $filter); + $count = $filter->countCourses(); if ($count > $this->max_show_courses && !Request::submitted('without_limit')) { $this->render_json([ @@ -526,6 +494,57 @@ class Admin_CoursesController extends AuthenticatedController $this->render_json($data); } + private function processFilters(): void + { + $filters = Request::getArray('filters'); + $config = User::findCurrent()->getConfiguration(); + + // Simple filters + $mapping = [ + 'search' => 'ADMIN_COURSES_SEARCHTEXT', + 'semester_id' => 'MY_COURSES_SELECTED_CYCLE', + 'stgteil' => 'MY_COURSES_SELECTED_STGTEIL', + 'teacher_filter' => 'ADMIN_COURSES_TEACHERFILTER', + 'course_type' => 'MY_COURSES_TYPE_FILTER', + 'institut_id' => 'MY_INSTITUTES_DEFAULT', + ]; + + foreach ($mapping as $key => $field) { + if (isset($filters[$key])) { + $config->store($field, $filters[$key]); + } + + unset($filters[$key]); + } + + // Datafield filters + $activeSidebarElements = $this->getActiveElements(); + + $datafields_filters = $GLOBALS['user']->cfg->ADMIN_COURSES_DATAFIELDS_FILTERS; + foreach (DataField::getDataFields('sem') as $datafield) { + $key = "df_{$datafield->id}"; + + if ( + !empty($filters[$key]) + && in_array($datafield->id, $activeSidebarElements['datafields']) + ) { + $datafields_filters[$datafield->id] = $filters[$key]; + } else { + unset($datafields_filters[$datafield->id]); + } + } + $config->store('ADMIN_COURSES_DATAFIELDS_FILTERS', $datafields_filters); + + // Plugin filters + foreach (PluginEngine::getPlugins(AdminCourseWidgetPlugin::class) as $plugin) { + $plugin_filters = array_intersect_key( + $filters, + $plugin->getFilters() + ); + $plugin->setFilters($plugin_filters); + } + } + protected function getCourseData(Course $course, $activated_fields) { $d = [ diff --git a/db/migrations/5.4.15_add_missing_configurations_used_in_admin_courses.php b/db/migrations/5.4.15_add_missing_configurations_used_in_admin_courses.php new file mode 100644 index 00000000000..64d26d55e6a --- /dev/null +++ b/db/migrations/5.4.15_add_missing_configurations_used_in_admin_courses.php @@ -0,0 +1,73 @@ +<?php + +/** + * @see https://gitlab.studip.de/studip/studip/-/merge_requests/2071/diffs#note_84752 + */ +final class AddMissingConfigurationsUsedInAdminCourses extends Migration +{ + public function description() + { + return 'Adds the missing configurations for ADMIN_COURSES_SEARCHTEXT, ' + . 'MY_COURSES_SELECTED_CYCLE, MY_COURSES_SELECTED_STGTEIL, ' + . 'ADMIN_COURSES_TEACHERFILTER and MY_COURSES_TYPE_FILTER.'; + } + + protected function up() + { + $query = "INSERT IGNORE INTO `config` ( + `field`, `value`, `type`, `range`, `section`, + `mkdate`, `chdate`, `description` + ) VALUES ( + :field, :value, :type, 'user', '', + UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description + )"; + $statement = DBManager::get()->prepare($query); + + $statement->execute([ + ':field' => 'ADMIN_COURSES_SEARCHTEXT', + ':value' => '', + ':type' => 'string', + ':description' => 'Speichert den auf der Veranstaltungsübersicht für Admins eingegebenen Suchtext', + ]); + + $statement->execute([ + ':field' => 'MY_COURSES_SELECTED_CYCLE', + ':value' => '', + ':type' => 'string', + ':description' => 'Das auf der Veranstaltungsübersicht für Admins gewählte Semester', + ]); + + $statement->execute([ + ':field' => 'MY_COURSES_SELECTED_STGTEIL', + ':value' => '', + ':type' => 'string', + ':description' => 'Der auf der Veranstaltungsübersicht für Admins gewählte Studiengangsteil', + ]); + + $statement->execute([ + ':field' => 'ADMIN_COURSES_TEACHERFILTER', + ':value' => '', + ':type' => 'string', + ':description' => 'Der auf der Veranstaltungsübersicht für Admins gewählte Filter auf Lehrende', + ]); + + $statement->execute([ + ':field' => 'MY_COURSES_TYPE_FILTER', + ':value' => '', + ':type' => 'string', + ':description' => 'Der auf der Veranstaltungsübersicht für Admins gewählte Filter auf Veranstaltungstypen', + ]); + } + + protected function down() + { + $query = "DELETE FROM `config` WHERE `field` = :field"; + $statement = DBManager::get()->prepare($query); + + $statement->execute([':field' => 'ADMIN_COURSES_SEARCHTEXT']); + $statement->execute([':field' => 'MY_COURSES_SELECTED_CYCLE']); + $statement->execute([':field' => 'MY_COURSES_SELECTED_STGTEIL']); + $statement->execute([':field' => 'ADMIN_COURSES_TEACHERFILTER']); + $statement->execute([':field' => 'MY_COURSES_TYPE_FILTER']); + } +} diff --git a/lib/classes/Widget.php b/lib/classes/Widget.php index f2dd3f06029..3ccdf56b671 100644 --- a/lib/classes/Widget.php +++ b/lib/classes/Widget.php @@ -41,25 +41,31 @@ class Widget /** * Add an element to the widget. * - * @param WidgetElement $element The actual element - * @param String $index Index/name of the element + * @template E of WidgetElement + * @param E $element The actual element + * @param String $index Index/name of the element + * @return E */ - public function addElement(WidgetElement $element, $index = null) + public function addElement(WidgetElement $element, $index = null): WidgetElement { $index = $index ?: $this->guessIndex($element); $this->elements[$index] = $element; + + return $element; } /** * Insert an element before a specific other element or at the end of the * list if the specified position is invalid. * - * @param WidgetElement $element The actual element + * @template E of WidgetElement + * @param E $element The actual element * @param String $before_index Insert element before this element. * @param String $index Index/name of the element + * @return E */ - public function insertElement(WidgetElement $element, $before_index, $index = null) + public function insertElement(WidgetElement $element, $before_index, $index = null): WidgetElement { $index = $index ?: $this->guessIndex($element); @@ -79,6 +85,8 @@ class Widget } $this->elements = $elements; + + return $element; } /** diff --git a/lib/classes/helpbar/HelpbarWidget.php b/lib/classes/helpbar/HelpbarWidget.php index 12b3850e916..4e02b947ee3 100644 --- a/lib/classes/helpbar/HelpbarWidget.php +++ b/lib/classes/helpbar/HelpbarWidget.php @@ -3,33 +3,8 @@ class HelpbarWidget extends Widget { public $icon = false; - public function addElement(WidgetElement $element, $index = null) - { - parent::addElement($element, $index); - } - public function setIcon($icon) { $this->icon = $icon; } - - /** - * Renders the widget. - * The widget will only be rendered if it contains at least one element. - * - * @return String The THML code of the rendered sidebar widget - */ - public function render($variables = []) - { - $content = ''; - - if ($this->hasElements()) { - $template = $GLOBALS['template_factory']->open($this->template); - $template->set_attributes($variables + $this->template_variables); - $template->elements = $this->elements; - $content = $template->render(); - } - - return $content; - } -} \ No newline at end of file +} diff --git a/lib/classes/sidebar/AdminCourseOptionsWidget.php b/lib/classes/sidebar/AdminCourseOptionsWidget.php new file mode 100644 index 00000000000..5565e2f75c7 --- /dev/null +++ b/lib/classes/sidebar/AdminCourseOptionsWidget.php @@ -0,0 +1,153 @@ +<?php +/** + * This is a special widget class for use with the admin course page. + * It will connect to the Vue app and use it's method to change the filters. + * + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @since Stud.IP 5.4 + */ +class AdminCourseOptionsWidget extends ListWidget +{ + /** + * @var string|null + */ + protected $position_in_sidebar = null; + + public function __construct(string $title) + { + parent::__construct(); + + $this->addCSSClass('widget-options admin-courses-options'); + $this->title = $title; + } + + /** + * Adds a checkbox to the widget. + * + * @param string $label + * @param string $filter_name + * @param bool $checked + * @param $true_value + * @param $false_value + * @param array $attributes + * + * @return ButtonElement + */ + public function addCheckbox( + string $label, + string $filter_name, + bool $checked = false, + $true_value = 1, + $false_value = null, + array $attributes = [] + ): ButtonElement { + if (!isset($attributes['onclick'])) { + $attributes['onclick'] = implode('', [ + sprintf( + 'STUDIP.AdminCourses.App.changeFilter({%s: $(this).is(".options-checked") ? %s : %s});', + json_encode($filter_name), + json_encode($true_value), + json_encode($false_value) + ), + "return false;", + ]); + } + + $attributes['class'] = trim(($attributes['class'] ?? '') . ' options-checkbox options-' . ($checked ? 'checked' : 'unchecked')); + + return $this->addElement( + new ButtonElement($label, null, $attributes + [ + 'aria-checked' => $checked ? 'true' : 'false', + 'role' => 'checkbox', + ]) + ); + } + + /** + * Adds a radio button to the widget. + * + * @param string $label + * @param string $filter_name + * @param $value + * @param bool $checked + * @param array $attributes + * + * @return ButtonElement + */ + public function addRadioButton( + string $label, + string $filter_name, + $value, + bool $checked = false, + array $attributes = [] + ): ButtonElement { + if (!isset($attributes['onclick'])) { + $attributes['onclick'] = implode('', [ + sprintf( + 'STUDIP.AdminCourses.App.changeFilter({%s: %s});', + json_encode($filter_name), + json_encode($value) + ), + "return false;", + ]); + } + + $attributes['class'] = trim(($attributes['class'] ?? '') . ' options-radio options-' . ($checked ? 'checked' : 'unchecked')); + + return $this->addElement( + new ButtonElement($label, null, $attributes + [ + 'aria-checked' => $checked ? 'true' : 'false', + 'data-filter-name' => $filter_name, + 'role' => 'radio', + ]) + ); + } + + /** + * Adds a select list to the widget. + * + * @param string $label + * @param string $filter_name + * @param array $options + * @param $selected_value + * @param array $attributes + */ + public function addSelect( + string $label, + string $filter_name, + array $options, + $selected_value = null, + array $attributes = [] + ): SelectListElement { + if (!isset($attributes['onchange'])) { + $attributes['onfocus'] = 'this.classList.remove("submit-upon-select");'; + + $attributes['onchange'] = sprintf( + 'STUDIP.AdminCourses.App.changeFilter({%s: this.value});', + json_encode($filter_name) + ); + } + + return $this->addElement( + new SelectListElement($label, $filter_name, $options, $selected_value, $attributes) + ); + } + + /** + * Sets the position where the widget should be inserted in the sidebar. + */ + public function setPositionInSidebar(?string $position): void + { + $this->position_in_sidebar = $position; + } + + /** + * Returns the position where the widget should be inserted in the sidebar. + * + * @return string|null + */ + public function getPositionInSidebar(): ?string + { + return $this->position_in_sidebar; + } +} diff --git a/lib/classes/sidebar/AttributesArrayAccessTrait.php b/lib/classes/sidebar/AttributesArrayAccessTrait.php new file mode 100644 index 00000000000..7bea8348f17 --- /dev/null +++ b/lib/classes/sidebar/AttributesArrayAccessTrait.php @@ -0,0 +1,44 @@ +<?php +trait AttributesArrayAccessTrait +{ + public $attributes = []; + + /** + * @todo Add bool return type when Stud.IP requires PHP8 minimal + */ + #[ReturnTypeWillChange] + public function offsetExists($offset) + { + return isset($this->attributes[$offset]); + } + + /** + * @param $offset + * @return mixed + * + * @todo Add mixed return type when Stud.IP requires PHP8 minimal + */ + #[ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->attributes[$offset]; + } + + /** + * @todo Add void return type when Stud.IP requires PHP8 minimal + */ + #[ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $this->attributes[$offset] = $value; + } + + /** + * @todo Add void return type when Stud.IP requires PHP8 minimal + */ + #[ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->attributes[$offset]); + } +} diff --git a/lib/classes/sidebar/ButtonElement.php b/lib/classes/sidebar/ButtonElement.php new file mode 100644 index 00000000000..f60fccfffbd --- /dev/null +++ b/lib/classes/sidebar/ButtonElement.php @@ -0,0 +1,26 @@ +<?php +class ButtonElement extends WidgetElement implements ArrayAccess +{ + use AttributesArrayAccessTrait; + + public $label; + public $icon = null; + + public function __construct(string $label, \Icon $icon = null, array $attributes = []) + { + parent::__construct(); + + $this->label = $label; + $this->icon = $icon; + $this->attributes = $attributes; + } + + public function render() + { + return sprintf( + '<button %s>%s</button>', + arrayToHtmlAttributes($this->attributes), + htmlReady($this->label) + ); + } +} diff --git a/lib/classes/sidebar/LinkElement.php b/lib/classes/sidebar/LinkElement.php index fccb9fae1a0..8476c3ec448 100644 --- a/lib/classes/sidebar/LinkElement.php +++ b/lib/classes/sidebar/LinkElement.php @@ -5,6 +5,8 @@ */ class LinkElement extends WidgetElement implements ArrayAccess { + use AttributesArrayAccessTrait; + /** * Create link by parsing a html chunk. * @@ -60,7 +62,6 @@ class LinkElement extends WidgetElement implements ArrayAccess public $label; public $icon; public $active = false; - public $attributes = []; public $as_button = false; /** @@ -243,45 +244,4 @@ class LinkElement extends WidgetElement implements ArrayAccess { return filter_var($url, FILTER_VALIDATE_URL) !== false; } - - // Array access for attributes - - /** - * @todo Add bool return type when Stud.IP requires PHP8 minimal - */ - #[ReturnTypeWillChange] - public function offsetExists($offset) - { - return isset($this->attributes[$offset]); - } - - /** - * @param $offset - * @return mixed - * - * @todo Add mixed return type when Stud.IP requires PHP8 minimal - */ - #[ReturnTypeWillChange] - public function offsetGet($offset) - { - return $this->attributes[$offset]; - } - - /** - * @todo Add void return type when Stud.IP requires PHP8 minimal - */ - #[ReturnTypeWillChange] - public function offsetSet($offset, $value) - { - $this->attributes[$offset] = $value; - } - - /** - * @todo Add void return type when Stud.IP requires PHP8 minimal - */ - #[ReturnTypeWillChange] - public function offsetUnset($offset) - { - unset($this->attributes[$offset]); - } } diff --git a/lib/classes/sidebar/OptionsWidget.php b/lib/classes/sidebar/OptionsWidget.php index 304a5ac26ca..8ef12358bb6 100644 --- a/lib/classes/sidebar/OptionsWidget.php +++ b/lib/classes/sidebar/OptionsWidget.php @@ -8,9 +8,9 @@ class OptionsWidget extends ListWidget const INDEX = 'options'; /** - * @param String $title Optional alternative title + * @param string|null $title Optional alternative title */ - public function __construct($title = null) + public function __construct(?string $title = null) { parent::__construct(); @@ -19,85 +19,90 @@ class OptionsWidget extends ListWidget } /** - * @param String $label - * @param bool $state - * @param String $toggle_url Url to execute the action - * @param String $toggle_url_off Optional alternative url to explicitely - * turn off the checkbox ($toggle_url will - * then act as $toggle_url_on) - * @param Array $attributes Optional additional attributes for the anchor + * @param string $label + * @param bool $state + * @param string $toggle_url Url to execute the action + * @param string|null $toggle_url_off Optional alternative url to explicitely + * turn off the checkbox ($toggle_url will + * then act as $toggle_url_on) + * @param Array $attributes Optional additional attributes for the anchor + * + * @return ButtonElement */ - public function addCheckbox($label, $state, $toggle_url, $toggle_url_off = null, array $attributes = []) - { + public function addCheckbox( + string $label, + bool $state, + string $toggle_url, + ?string $toggle_url_off = null, + array $attributes = [] + ): ButtonElement { // TODO: Remove this some versions after 5.0 $toggle_url = html_entity_decode($toggle_url); $toggle_url_off = isset($toggle_url_off) ? html_entity_decode($toggle_url_off) : null; - $content = sprintf( - '<button formaction="%s" role="checkbox" aria-checked="%s" class="options-checkbox options-%s" %s>%s</button>', - htmlReady($state && $toggle_url_off !== null ? $toggle_url_off : $toggle_url), - $state ? 'true' : 'false', - $state ? 'checked' : 'unchecked', - arrayToHtmlAttributes($attributes), - htmlReady($label) + $attributes['class'] = trim(($attributes['class'] ?? '') . ' options-checkbox options-' . ($state ? 'checked' : 'unchecked')); + + return $this->addElement( + new ButtonElement($label, null, $attributes + [ + 'aria-checked' => $state ? 'true' : 'false', + 'formaction' => $state && $toggle_url_off !== null ? $toggle_url_off : $toggle_url, + 'role' => 'checkbox', + ]) ); - $this->addElement(new WidgetElement($content)); } /** - * @param String $label - * @param String $url + * Adds a radio button to the widget. + * + * @param string $label + * @param string $url * @param bool $checked + * @param array $attributes + * + * @return ButtonElement */ - public function addRadioButton($label, $url, $checked = false, array $attributes = []) - { + public function addRadioButton( + string $label, + string $url, + bool $checked = false, + array $attributes = [] + ): ButtonElement { // TODO: Remove this some versions after 5.0 $url = html_entity_decode($url); - $content = sprintf( - '<button formaction="%s" role="radio" aria-checked="%s" class="options-radio options-%s" %s>%s</button>', - htmlReady($url), - $checked ? 'true' : 'false', - $checked ? 'checked' : 'unchecked', - arrayToHtmlAttributes($attributes), - htmlReady($label) + $attributes['class'] = trim(($attributes['class'] ?? '') . ' options-radio options-' . ($checked ? 'checked' : 'unchecked')); + + return $this->addElement( + new ButtonElement($label, null, $attributes + [ + 'aria-checked' => $checked ? 'true' : 'false', + 'formaction' => $url, + 'role' => 'radio', + ]) ); - $this->addElement(new WidgetElement($content)); } /** * Adds a select element to the widget. * - * @param String $label - * @param String $url - * @param String $name Attribute name + * @param string $label + * @param string $url + * @param string $name Attribute name * @param array $options Array of associative options (value => label) * @param mixed $selected_option Currently selected option * @param array $attributes Additional attributes */ - public function addSelect($label, $url, $name, $options, $selected_option = false, $attributes = []) - { - $option_content = ''; + public function addSelect( + string $label, + string $url, + string $name, + array $options, + $selected_option = false, + array $attributes = [] + ): SelectListElement { + $attributes['data-formaction'] = $url; - foreach ($options as $value => $option) { - $selected = $value === $selected_option ? 'selected' : ''; - $option_content .= sprintf( - '<option value="%s" %s>%s</option>', - htmlReady($value), - $selected, - htmlReady($option) - ); - } - - $content = sprintf( - '<select data-formaction="%s" class="sidebar-selectlist submit-upon-select" name="%s" aria-label="%s" %s>%s</select>', - htmlReady($url), - htmlReady($name), - htmlReady($label), - arrayToHtmlAttributes($attributes), - $option_content + return $this->addElement( + new SelectListElement($label, $name, $options, $selected_option, $attributes) ); - - $this->addElement(new WidgetElement($content)); } } diff --git a/lib/classes/sidebar/SelectListElement.php b/lib/classes/sidebar/SelectListElement.php new file mode 100644 index 00000000000..9cbb42c4ef2 --- /dev/null +++ b/lib/classes/sidebar/SelectListElement.php @@ -0,0 +1,55 @@ +<?php +class SelectListElement extends WidgetElement implements ArrayAccess +{ + use AttributesArrayAccessTrait; + + protected $label; + protected $name; + protected $options; + protected $selected_option; + + public function __construct( + string $label, + string $name, + array $options, + $selected_option = null, + array $attributes = [] + ) { + $this->label = $label; + $this->name = $name; + $this->options = $options; + $this->selected_option = $selected_option; + $this->attributes = $attributes; + } + + public function setOptions(array $options): void + { + $this->options = $options; + } + + public function render() + { + $option_content = ''; + + foreach ($this->options as $value => $option) { + $selected = $value == $this->selected_option ? 'selected' : ''; + $option_content .= sprintf( + '<option value="%s" %s>%s</option>', + htmlReady($value), + $selected, + htmlReady($option) + ); + } + + $attributes = $this->attributes; + $attributes['class'] = trim(($attributes['class'] ?? '') . ' sidebar-selectlist submit-upon-select'); + $attributes['name'] = $this->name; + $attributes['aria-label'] = $this->label; + + return sprintf( + '<select %s>%s</select>', + arrayToHtmlAttributes($attributes), + $option_content + ); + } +} diff --git a/lib/plugins/core/AdminCourseWidgetPlugin.class.php b/lib/plugins/core/AdminCourseWidgetPlugin.class.php new file mode 100644 index 00000000000..c554626b23c --- /dev/null +++ b/lib/plugins/core/AdminCourseWidgetPlugin.class.php @@ -0,0 +1,42 @@ +<?php +/** + * This plugin interface is used to add functionality to the sidebar of the + * admin courses page. + * + * @see AdminCourseOptionsWidget + * @author Jan-Hendrik Willms <tleilax+studip@gmail.com> + * @since Stud.IP 5.4 + */ +interface AdminCourseWidgetPlugin +{ + /** + * Returns a list of widgets for the admin courses page. + * + * @return AdminCourseOptionsWidget[] + */ + public function getWidgets(): iterable; + + /** + * Return the filter values this widget provides. Return an associative + * array with filter names as indices and filter values as values. + * + * @return array + */ + public function getFilters(): array; + + /** + * Apply the set filters to the AdminCourseFilter query. + * + * @param AdminCourseFilter $filter + */ + public function applyFilters(AdminCourseFilter $filter): void; + + + /** + * Set filters from the admin course page. You will be given an associative + * array according to getFilters(). + * + * @param array $filters + */ + public function setFilters(array $filters): void; +} diff --git a/lib/plugins/engine/PluginEngine.class.php b/lib/plugins/engine/PluginEngine.class.php index 1d3e5a9b6b1..ecf00cff790 100644 --- a/lib/plugins/engine/PluginEngine.class.php +++ b/lib/plugins/engine/PluginEngine.class.php @@ -70,9 +70,10 @@ class PluginEngine * returns all enabled plugins. The optional context parameter can be * used to get only plugins that are activated in the given context. * - * @param string $type plugin type or null (all types) + * @template T + * @param T $type plugin type or null (all types) * @param string $context context range id (optional) - * @return array all plugins of the specified type + * @return T[] all plugins of the specified type */ public static function getPlugins ($type, $context = null) { diff --git a/resources/assets/javascripts/bootstrap/admin-courses.js b/resources/assets/javascripts/bootstrap/admin-courses.js index 06c4621ad63..74802b6ae4d 100644 --- a/resources/assets/javascripts/bootstrap/admin-courses.js +++ b/resources/assets/javascripts/bootstrap/admin-courses.js @@ -22,4 +22,20 @@ STUDIP.domReady(() => { STUDIP.AdminCourses.App = vm.$refs.app; }); + + + + $('.admin-courses-options').find('.options-radio, .options-checkbox').on('click', function () { + $(this).toggleClass(['options-checked', 'options-unchecked']); + $(this).attr('aria-checked', $(this).is('.options-checked') ? 'true' : 'false'); + + if ($(this).is('.options-radio')) { + const filterName = $(this).data('filter-name'); + $(`button[data-filter-name="${filterName}"]`) + .not(this) + .removeClass('options-checked') + .addClass('options-unchecked') + .attr('aria-checked', 'false'); + } + }); }); diff --git a/resources/vue/store/AdminCoursesStore.js b/resources/vue/store/AdminCoursesStore.js index 813644f1434..5b20e7013cb 100644 --- a/resources/vue/store/AdminCoursesStore.js +++ b/resources/vue/store/AdminCoursesStore.js @@ -81,7 +81,7 @@ export default { Screenreader.notify(''); let params = { - ...state.filters, + filters: state.filters, action: state.actionArea, activated_fields: state.activatedFields, without_limit: withoutLimit ? 1 : null, diff --git a/templates/sidebar/list-widget.php b/templates/sidebar/list-widget.php index 5c94a0ed149..4085cf6a130 100644 --- a/templates/sidebar/list-widget.php +++ b/templates/sidebar/list-widget.php @@ -2,12 +2,9 @@ <?= CSRFProtection::tokenTag() ?> <ul class="<?= implode(' ', $css_classes) ?>" aria-label="<?= htmlReady($title) ?>"> <? foreach ($elements as $index => $element): ?> - <? $icon = null ?> - <? if ($element instanceof LinkElement): ?> - <? $icon = $element->icon ?? null ?> - <? if ($icon && $element->isDisabled()): ?> - <? $icon = $icon->copyWithRole('inactive') ?> - <? endif ?> + <? $icon = $element->icon ?? null ?> + <? if ($icon && $element instanceof LinkElement && $element->isDisabled()): ?> + <? $icon = $icon->copyWithRole(Icon::ROLE_INACTIVE) ?> <? endif ?> <li id="<?= htmlReady($index) ?>" <?= isset($icon) ? 'style="' . $icon->asCSS() .'"' : '' ?> -- GitLab