diff --git a/app/controllers/admin/plugin.php b/app/controllers/admin/plugin.php index 7a87d8fe275ad8ed04194625abc082de98c278f5..ef635175640ed319bc17b0c973b4fd7cd1166f55 100644 --- a/app/controllers/admin/plugin.php +++ b/app/controllers/admin/plugin.php @@ -571,4 +571,49 @@ class Admin_PluginController extends AuthenticatedController } } + public function edit_description_action(Plugin $plugin) + { + $this->plugin = PluginManager::getInstance()->getPluginById($plugin->getId()); + $this->metadata = $this->plugin->getMetadata(); + $this->form = \Studip\Forms\Form::fromSORM($plugin, [ + 'legend' => _('Pluginbeschreibung'), + 'fields' => [ + 'description' => [ + 'label' => _('Beschreibung'), + 'type' => 'i18n_formatted' + ], + 'manifest_info_de' => [ + 'label' => _('Standardbeschreibung des Plugins'), + 'type' => 'info', + 'value' => $this->metadata['descriptionlong'] ?? $this->metadata['description'], + 'if' => "STUDIPFORM_SELECTEDLANGUAGES.description === 'de_DE'" + ], + 'manifest_info_en' => [ + 'label' => sprintf(_('Standardbeschreibung des Plugins (%s)'), _('Englisch')), + 'type' => 'info', + 'value' => $this->metadata['descriptionlong_en'] ?? $this->metadata['description_en'], + 'if' => "STUDIPFORM_SELECTEDLANGUAGES.description === 'en_GB'" + ], + 'decription_mode' => [ + 'label' => _('Modus der neuen Beschreibung'), + 'type' => 'select', + 'options' => [ + 'add' => _('Hinzufügen zur Standardbeschreibung'), + 'override_description' => _('Standardbeschreibung überschreiben'), + 'replace_all' => _('Beschreibungsfenster komplett ersetzen durch Beschreibung') + ] + ], + 'highlight_until' => [ + 'label' => _('In Veranstaltungen bewerben bis (oder leer lassen)'), + 'type' => 'datetimepicker' + ], + 'highlight_text' => [ + 'label' => _('Bewerbungs-Infotext') + ] + ] + ])->autoStore() + //->setDebugMode(true) + ->setURL(URLHelper::getURL('dispatch.php/admin/plugin/index')); + } + } diff --git a/app/controllers/course/basicdata.php b/app/controllers/course/basicdata.php index b9ba67e793b57efaa40fa69cd21d7b462ba7e359..937ec0f0d0249c9f5c1a2136289496b59235db37 100644 --- a/app/controllers/course/basicdata.php +++ b/app/controllers/course/basicdata.php @@ -290,7 +290,8 @@ class Course_BasicdataController extends AuthenticatedController } //Daten sammeln: - $sem = Seminar::getInstance($this->course_id); + $course = Course::find($this->course_id); + $sem = new Seminar($course); $data = $sem->getData(); //Erster, zweiter und vierter Reiter des Akkordions: Grundeinstellungen @@ -365,10 +366,51 @@ class Course_BasicdataController extends AuthenticatedController $widget = new ActionsWidget(); + $sem_create_perm = in_array(Config::get()->SEM_CREATE_PERM, ['root','admin','dozent']) ? Config::get()->SEM_CREATE_PERM : 'dozent'; + if ($GLOBALS['perm']->have_perm($sem_create_perm)) { + if (!LockRules::check(Context::getId(), 'seminar_copy')) { + $widget->addLink( + _('Veranstaltung kopieren'), + $this->url_for( + 'course/wizard/copy/' . $this->course_id, + ['studip_ticket' => Seminar_Session::get_ticket()] + ), + Icon::create('seminar') + ); + } + } $widget->addLink(_('Bild ändern'), - $this->url_for('avatar/update/course', $course_id), - Icon::create('edit') + $this->url_for('avatar/update/course', $this->course_id), + Icon::create('edit') ); + if ($GLOBALS['perm']->have_perm('admin')) { + $is_locked = $course->lock_rule; + $widget->addLink( + _('Sperrebene ändern') . ' (' . ($is_locked ? _('gesperrt') : _('nicht gesperrt')) . ')', + $this->url_for( + 'course/management/lock', + ['studip_ticket' => Seminar_Session::get_ticket()] + ), + Icon::create('lock-' . ($is_locked ? 'locked' : 'unlocked')) + )->asDialog('size=auto'); + } + + if ( + (Config::get()->ALLOW_DOZENT_VISIBILITY || $GLOBALS['perm']->have_perm('admin')) + && !LockRules::Check($this->course_id, 'seminar_visibility') + ) { + $is_visible = $course->visible; + if ($course->isOpenEnded() || $course->end_semester->visible) { + $widget->addLink( + $is_visible ? _('Veranstaltung verstecken') : _('Veranstaltung sichtbar schalten'), + $this->url_for( + 'course/management/change_visibility', + ['studip_ticket' => Seminar_Session::get_ticket()] + ), + Icon::create('visibility-' . ($is_visible ? 'visible' : 'invisible')) + ); + } + } if ($this->deputies_enabled) { if (Deputy::isDeputy($GLOBALS['user']->id, $this->course_id)) { @@ -388,6 +430,16 @@ class Course_BasicdataController extends AuthenticatedController ); } } + if (Config::get()->ALLOW_DOZENT_DELETE || $GLOBALS['perm']->have_perm('admin')) { + $widget->addLink( + _('Veranstaltung löschen'), + $this->url_for( + 'course/archive/confirm', + ['studip_ticket' => Seminar_Session::get_ticket()] + ), + Icon::create('trash') + )->asDialog('size=auto'); + } $sidebar->addWidget($widget); if ($GLOBALS['perm']->have_studip_perm('admin', $this->course_id)) { $widget = new CourseManagementSelectWidget(); diff --git a/app/controllers/course/change_view.php b/app/controllers/course/change_view.php index 58cc9957880b4821964df53475643d99153f8e61..156a68a8fc77f88d6b64580bfeb8f893165bd419 100644 --- a/app/controllers/course/change_view.php +++ b/app/controllers/course/change_view.php @@ -48,6 +48,6 @@ class Course_ChangeViewController extends AuthenticatedController public function reset_changed_view_action() { unset($_SESSION["seminar_change_view_{$this->course_id}"]); - $this->relocate('course/management'); + $this->relocate('course/contentmodules'); } } diff --git a/app/controllers/course/contentmodules.php b/app/controllers/course/contentmodules.php new file mode 100644 index 0000000000000000000000000000000000000000..2e4907a7e25cb9fea5f0a6e9fc0919f9a7c109a5 --- /dev/null +++ b/app/controllers/course/contentmodules.php @@ -0,0 +1,293 @@ +<?php + +class Course_ContentmodulesController extends AuthenticatedController +{ + public function index_action() + { + Navigation::activateItem('/course/admin/contentmodules'); + PageLayout::setTitle(_('Werkzeuge')); + + if (Context::isCourse()) { + $this->sem = Context::get(); + $this->sem_class = $this->sem->getSemClass(); + } else { + $this->sem = Context::get(); + $this->sem_class = SemClass::getDefaultInstituteClass($this->sem['type']); + } + $this->modules = $this->getModules($this->sem); + + $this->highlighted_modules = []; + foreach ($this->modules as $module) { + if ($module['highlighted']) { + $this->highlighted_modules[] = $module['id']; + } + } + + if (Context::isCourse()) { + $actions = new ActionsWidget(); + + $actions->addLink( + _('Studierendenansicht simulieren'), + URLHelper::getURL('dispatch.php/course/change_view/set_changed_view'), + Icon::create('visibility-invisible') + ); + Sidebar::Get()->addWidget($actions); + } + + $views = Sidebar::Get()->addWidget(new ViewsWidget()); + $views->id = 'tool-view-switch'; + $views->addLink( + _('Kachelansicht'), + '#tiles' + )->setActive($GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY); + $views->addLink( + _('Tabellarische Ansicht'), + '#tabular' + )->setActive(!$GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY); + + $this->categories = []; + foreach ($this->modules as $i => $module) { + if ($module['category'] && !in_array($module['category'], $this->categories)) { + $this->categories[] = $module['category']; + } + if (!$module['category']) { + if (!in_array(_('Sonstige'), $this->categories)) { + $this->categories[] = _('Sonstige'); + } + $this->modules[$i]['category'] = _('Sonstige'); + } + } + sort($this->categories); + + $filter_widget = Sidebar::Get()->addWidget(new OptionsWidget()); + $filter_widget->id = 'tool-filter-category'; + $filter_widget->setTitle(_('Filter nach Kategorie')); + $filter_widget->addRadioButton( + _('Alle Kategorien'), + '#', + true + ); + foreach ($this->categories as $category) { + $filter_widget->addRadioButton( + $category, + '#' + ); + } + + if ( + Context::isCourse() + && $GLOBALS['perm']->have_studip_perm('admin', Context::getId()) + && !$this->sem_class['studygroup_mode'] + ) { + $widget = new CourseManagementSelectWidget(); + Sidebar::Get()->addWidget($widget); + } + + PageLayout::addHeadElement('script', [ + 'type' => 'text/javascript', + ], sprintf( + 'window.ContentModulesStoreData = %s;', + json_encode([ + 'setCategories' => $this->categories, + 'setHighlighted' => $this->highlighted_modules, + 'setModules' => array_values($this->modules), + 'setUserId' => User::findCurrent()->id, + 'setView' => $GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY ? 'tiles' : 'table', + ]) + )); + } + + public function trigger_action() + { + $context = Context::get(); + + $required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin'; + if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) { + throw new AccessDeniedException(); + } + if (Request::isPost()) { + if ($context->getRangeType() === 'course') { + $sem_class = $context->getSemClass(); + } else { + $sem_class = SemClass::getDefaultInstituteClass($context->type); + } + $moduleclass = Request::get('moduleclass'); + $active = Request::bool('active', false); + $module = new $moduleclass; + if ($module->isActivatableForContext($context)) { + PluginManager::getInstance()->setPluginActivated($module->getPluginId(), $context->getId(), $active); + } + if ($active) { + $active_tool = ToolActivation::find([$context->id, $module->getPluginId()]); + $default_position = array_search(get_class($module), $sem_class->getActivatedModules()); + if ($default_position !== false && $active_tool) { + $active_tool->position = $default_position; + $active_tool->store(); + } + } + //$this->redirect("course/contentmodules/trigger", ['cid' => $context->getId()]); + } + $template = $GLOBALS['template_factory']->open('tabs.php'); + $template->navigation = Navigation::getItem('/course'); + Navigation::getItem('/course/admin')->setActive(true); + $this->render_json([ + 'tabs' => $template->render(), + 'position' => $active_tool->position + ]); + } + + public function reorder_action() + { + $context = Context::get(); + + $required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin'; + if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) { + throw new AccessDeniedException(); + } + if (Request::isPost()) { + $position = 0; + foreach (Request::getArray('order') as $plugin_id) { + $tool = ToolActivation::find([$context->getId(), $plugin_id]); + $tool->position = $position++; + $tool->store(); + } + $this->redirect($this->reorderURL()); + return; + } + Navigation::getItem('/course/admin')->setActive(true); + $template = $GLOBALS['template_factory']->open('tabs.php'); + $template->navigation = Navigation::getItem('/course'); + $this->render_json([ + 'tabs' => $template->render() + ]); + } + + public function change_visibility_action() + { + if (!Request::isPost()) { + throw new AccessDeniedException(); + } + $context = Context::get(); + + $required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin'; + if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) { + throw new AccessDeniedException(); + } + $moduleclass = Request::get('moduleclass'); + $module = new $moduleclass; + + $active_tool = ToolActivation::find([$context->id, $module->getPluginId()]); + $metadata = $active_tool->metadata->getArrayCopy(); + if (Request::bool('visible')) { + unset($metadata['visibility']); + } else { + $metadata['visibility'] = 'tutor'; + } + $active_tool['metadata'] = $metadata; + $active_tool->store(); + + $this->render_json([ + 'visibility' => $active_tool->getVisibilityPermission() + ]); + } + + public function tiles_display_action() + { + if (Request::isPost()) { + $GLOBALS['user']->cfg->store( + 'CONTENTMODULES_TILED_DISPLAY', + Request::get('view') === 'tiles' + ); + } + $this->render_nothing(); + } + + public function rename_action($module_id) + { + $context = Context::get(); + + $required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin'; + if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) { + throw new AccessDeniedException(); + } + $this->module = PluginManager::getInstance()->getPluginById($module_id); + $this->metadata = $this->module->getMetadata(); + PageLayout::setTitle(_('Werkzeug umbenennen')); + $this->tool = ToolActivation::find([$context->id, $module_id]); + if (Request::isPost()) { + $metadata = $this->tool->metadata->getArrayCopy(); + if (!trim(Request::get('displayname')) || Request::submitted('delete')) { + unset($metadata['displayname']); + } else { + $metadata['displayname'] = trim(Request::get('displayname')); + } + $this->tool['metadata'] = $metadata; + $this->tool->store(); + $this->redirect('course/contentmodules/index'); + } + } + + public function info_action($plugin_id) + { + $this->plugin = PluginManager::getInstance()->getPluginById($plugin_id); + $this->metadata = $this->plugin->getMetadata(); + PageLayout::setTitle(sprintf(_('Informationen über %s'), $this->metadata['displayname'])); + } + + private function getModules(Range $context) + { + $list = []; + + foreach (PluginEngine::getPlugins('StudipModule') as $plugin) { + if (!$plugin->isActivatableForContext($context)) { + continue; + } + + if (!$this->sem_class->isModuleAllowed(get_class($plugin))) { + continue; + } + + $info = $plugin->getMetadata(); + + $plugin_id = $plugin->getPluginId(); + + $tool = ToolActivation::find([$context->getRangeId(), $plugin->getPluginId()]); + $toolname = $info['displayname'] ?? $plugin->getPluginname(); + if ($tool && $tool->metadata['displayname']) { + $displayname = $tool->getDisplayname() . ' (' . $toolname . ')'; + } else { + $displayname = $toolname; + } + $visibility = $tool ? $tool->getVisibilityPermission() : 'nobody'; + + $metadata = $plugin->getMetadata(); + $list[$plugin_id] = [ + 'id' => $plugin_id, + 'moduleclass' => get_class($plugin), + 'position' => $tool ? $tool->position : null, + 'toolname' => $toolname, + 'displayname' => $displayname, + 'visibility' => $visibility, + 'active' => (bool) $tool, + ]; + if ($metadata['icon_clickable']) { + $list[$plugin_id]['icon'] = $metadata['icon_clickable'] instanceof Icon + ? $metadata['icon_clickable']->asImagePath() + : Icon::create($plugin->getPluginURL().'/'.$metadata['icon_clickable'])->asImagePath(); + } elseif ($metadata['icon']) { + $list[$plugin_id]['icon'] = $metadata['icon'] instanceof Icon + ? $metadata['icon']->asImagePath() + : Icon::create($plugin->getPluginURL().'/'.$metadata['icon'])->asImagePath(); + } else { + $list[$plugin_id]['icon'] = null; + } + $list[$plugin_id]['summary'] = $metadata['summary']; + $list[$plugin_id]['mandatory'] = $this->sem_class->isModuleMandatory(get_class($plugin)); + $list[$plugin_id]['highlighted'] = (bool) $plugin->isHighlighted(); + $list[$plugin_id]['highlight_text'] = $plugin->getHighlightText(); + $list[$plugin_id]['category'] = $metadata['category']; + } + + return $list; + } +} diff --git a/app/controllers/course/plus.php b/app/controllers/course/plus.php deleted file mode 100644 index 1a5ac26b1b876ad857c8066e2fa81abc305b4d67..0000000000000000000000000000000000000000 --- a/app/controllers/course/plus.php +++ /dev/null @@ -1,399 +0,0 @@ -<?php -# Lifter001: TODO -/* - * Copyright (C) 2012 - Rasmus Fuhse <fuhse@data-quest.de> - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - */ - -use Studip\Button, Studip\LinkButton; - -class Course_PlusController extends AuthenticatedController -{ - - public function before_filter(&$action, &$args) - { - parent::before_filter($action, $args); - $id = Context::get()->getId(); - $object_type = Context::getType(); - if (!$id || !$GLOBALS['perm']->have_studip_perm($object_type === 'course' ? 'tutor' : 'admin', $id)) { - throw new AccessDeniedException(); - } - Navigation::activateItem('/course/modules'); - - if ($object_type === 'course') { - $this->sem = Context::get(); - $this->sem_class = $this->sem->getSemClass(); - } else { - $this->sem = Context::get(); - $this->sem_class = SemClass::getDefaultInstituteClass($this->sem['type']); - } - PageLayout::setTitle(_("Mehr Funktionen")); - } - - public function index_action() - { - - PageLayout::setTitle($this->sem->getFullname() . " - " . PageLayout::getTitle()); - PageLayout::addSqueezePackage('statusgroups'); //sortier css - - $this->setupSidebar(); - $this->available_modules = $this->getSortedList($this->sem); - - if (Request::submitted('deleteContent')) { - $this->deleteContent($this->available_modules); - } - } - - public function trigger_action() - { - $context = Context::get(); - - if (!$GLOBALS['perm']->have_studip_perm($context->getRangeType() === 'course' ? 'tutor' : 'admin', $context->getId())) { - throw new AccessDeniedException(); - } - if (Request::isPost()) { - if ($context->getRangeType() === 'course') { - $sem_class = $context->getSemClass(); - } else { - $sem_class = SemClass::getDefaultInstituteClass($context->type); - } - $moduleclass = Request::get("moduleclass"); - $active = Request::int("active", 0); - $module = new $moduleclass; - if ($module->isActivatableForContext($context)) { - PluginManager::getInstance()->setPluginActivated($module->getPluginId(), $context->getId(), $active); - if (Context::isCourse()) { - if ($active) { - StudipLog::log('PLUGIN_ENABLE', Context::getId(), $module->getPluginId(), $GLOBALS['user']->id); - NotificationCenter::postNotification('PluginDidActivate', Context::getId(), $module->getPluginId()); - } else { - StudipLog::log('PLUGIN_DISABLE', Context::getId(), $module->getPluginId(), $GLOBALS['user']->id); - NotificationCenter::postNotification('PluginDidDeactivate', Context::getId(), $module->getPluginId()); - } - } - } - if ($active) { - $default_position = array_search(get_class($module), $sem_class->getActivatedModules()); - if ($default_position !== false) { - $active_tool = ToolActivation::find([$context->getId(), $module->getPluginId()]); - if ($active_tool) { - $active_tool->position = $default_position; - $active_tool->store(); - } - } - } - $this->redirect("course/plus/trigger", ['cid' => $context->getId()]); - } else { - $template = $GLOBALS['template_factory']->open("tabs.php"); - $template->navigation = Navigation::getItem("/course"); - $this->render_json([ - 'tabs' => $template->render() - ]); - } - } - - public function sorttools_action() - { - PageLayout::setTitle(_('Reihenfolge der Werkzeuge ändern')); - if (Request::submitted('order')) { - CSRFProtection::verifyUnsafeRequest(); - $plugin_id = explode('_', Request::get('id'))[1]; - $newpos = Request::get('index') + 1; - if ($this->sem->tools->findOneBy('plugin_id', $plugin_id)) { - $oldpos = $this->sem->tools->findOneBy('plugin_id', $plugin_id)->position; - if ($oldpos < $newpos) { - $this->sem->tools->findBy('position', $newpos, '>')->each(function ($p) { - $p->position++; - }); - $this->sem->tools->findOneBy('plugin_id', $plugin_id)->position = $newpos + 1; - } else { - $this->sem->tools->findBy('position', $newpos, '>=')->each(function ($p) { - $p->position++; - }); - $this->sem->tools->findOneBy('plugin_id', $plugin_id)->position = $newpos; - } - $this->sem->tools->orderBy('position asc')->each(function ($p) {static $pos = 0; $p->position = $pos++;}); - $this->sem->tools->store(); - $this->render_nothing(); - return; - } - } - - } - - public function edittool_action($plugin) - { - PageLayout::setTitle(_('Optionen des Werkzeugs ändern')); - $id = explode('_', $plugin)[1]; - $this->tool = ToolActivation::find([$this->sem->id, $id]); - if (!$this->tool) { - $this->render_nothing(); - return; - } - if (Request::submitted('save')) { - CSRFProtection::verifyUnsafeRequest(); - $displayname = trim(Request::get('displayname')); - if ($displayname !== $this->tool->getDisplayname()) { - if (strlen($displayname)) { - $this->tool->metadata['displayname'] = $displayname; - } else { - unset($this->tool->metadata['displayname']); - } - - } - if (Request::get('permission') === 'tutor') { - $this->tool->metadata['visibility'] = 'tutor'; - } else { - unset($this->tool->metadata['visibility']); - } - if ($this->tool->store()) { - PageLayout::postSuccess(_('Die Einstellungen wurden gespeichert.')); - } - $this->redirect($this->action_url('index')); - } - } - - - private function deleteContent($plugmodlist) - { - $name = Request::get('name'); - - foreach ($plugmodlist as $key => $val) { - if (array_key_exists($name, $val)) { - if ($val[$name]['type'] == 'plugin') { - $class = PluginEngine::getPlugin(get_class($val[$name]['object'])); - $displayname = $class->getPluginName(); - } - } - } - - if (Request::submitted('check')) { - if (method_exists($class, 'deleteContent')) { - $class->deleteContent(); - } else { - PageLayout::postMessage(MessageBox::info(_("Das Plugin/Modul enthält keine Funktion zum Löschen der Inhalte."))); - } - } else { - PageLayout::postMessage(MessageBox::info(sprintf(_("Sie beabsichtigen die Inhalte von %s zu löschen."), htmlReady($displayname)) - . "<br>" . _("Wollen Sie die Inhalte wirklich löschen?") . "<br>" - . LinkButton::createAccept(_('Ja'), URLHelper::getURL("?deleteContent=true&check=true&name=" . $name)) - . LinkButton::createCancel(_('Nein')))); - } - } - - private function setupSidebar() - { - - $plusconfig = UserConfig::get($GLOBALS['user']->id)->PLUS_SETTINGS; - - if (!isset($_SESSION['plus'])) { - if (isset($plusconfig['course_plus'])){ - $usr_conf = $plusconfig['course_plus']; - - $_SESSION['plus']['Kategorie']['Lehr- und Lernorganisation'] = $usr_conf['Kategorie']['Lehr- und Lernorganisation']; - $_SESSION['plus']['Kategorie']['Kommunikation und Zusammenarbeit'] = $usr_conf['Kategorie']['Kommunikation und Zusammenarbeit']; - $_SESSION['plus']['Kategorie']['Inhalte und Aufgabenstellungen'] = $usr_conf['Kategorie']['Inhalte und Aufgabenstellungen']; - $_SESSION['plus']['Kategorie']['Sonstiges'] = $usr_conf['Kategorie']['Sonstiges']; - - foreach ($usr_conf['Kategorie'] as $key => $val){ - if(!array_key_exists($key, $_SESSION['plus']['Kategorie'])){ - $_SESSION['plus']['Kategorie'][$key] = $val; - } - } - - $_SESSION['plus']['View'] = $usr_conf['View']; - $_SESSION['plus']['displaystyle'] = $usr_conf['displaystyle']; - - } else { - $_SESSION['plus']['Kategorie']['Lehr- und Lernorganisation'] = 1; - $_SESSION['plus']['Kategorie']['Kommunikation und Zusammenarbeit'] = 1; - $_SESSION['plus']['Kategorie']['Inhalte und Aufgabenstellungen'] = 1; - $_SESSION['plus']['Kategorie']['Sonstiges'] = 1; - $_SESSION['plus']['View'] = 'openall'; - $_SESSION['plus']['displaystyle'] = 'category'; - } - } - - if(isset($_SESSION['plus']['Kategorielist'])){ - foreach ($_SESSION['plus']['Kategorie'] as $key => $val){ - if(!array_key_exists($key, $_SESSION['plus']['Kategorielist']) && $key != 'Sonstiges'){ - unset($_SESSION['plus']['Kategorie'][$key]); - } - } - } - if (Request::get('mode') !== null) { - $_SESSION['plus']['View'] = Request::get('mode'); - } - if (Request::get('displaystyle') !== null) { - $_SESSION['plus']['displaystyle'] = Request::get('displaystyle'); - } - - $sidebar = Sidebar::get(); - - $widget = new OptionsWidget(); - $widget->setTitle(_('Kategorien')); - - foreach ($_SESSION['plus']['Kategorie'] as $key => $val) { - - if (Request::get(md5('cat_' . $key)) !== null) { - $_SESSION['plus']['Kategorie'][$key] = Request::get(md5('cat_' . $key)); - } - - if ($_SESSION['plus']['displaystyle'] == 'alphabetical') { - $_SESSION['plus']['Kategorie'][$key] = 1; - } - - if ($key == 'Sonstiges') { - continue; - } - $widget->addCheckbox( - $key, - $_SESSION['plus']['Kategorie'][$key], - URLHelper::getURL('?', [md5('cat_' . $key) => 1, 'displaystyle' => 'category']), - URLHelper::getURL('?', [md5('cat_' . $key) => 0, 'displaystyle' => 'category']) - ); - - } - - $widget->addCheckbox( - _('Sonstiges'), - $_SESSION['plus']['Kategorie']['Sonstiges'], - URLHelper::getURL('?', [md5('cat_Sonstiges') => 1, 'displaystyle' => 'category']), - URLHelper::getURL('?', [md5('cat_Sonstiges') => 0, 'displaystyle' => 'category']) - ); - - $sidebar->addWidget($widget, 'Kategorien'); - - $widget = new ActionsWidget(); - $widget->setTitle(_('Ansichten')); - - if ($_SESSION['plus']['View'] === 'openall') { - $widget->addLink( - _('Alles zuklappen'), - URLHelper::getURL('?', ['mode' => 'closeall']), - Icon::create('assessment') - ); - } else { - $widget->addLink( - _('Alles aufklappen'), - URLHelper::getURL('?', ['mode' => 'openall']), - Icon::create('assessment') - ); - } - - if ($_SESSION['plus']['displaystyle'] === 'category') { - $widget->addLink( - _('Alphabetische Anzeige ohne Kategorien'), - URLHelper::getURL('?', ['displaystyle' => 'alphabetical']), - Icon::create('assessment') - ); - } else { - $widget->addLink( - _('Anzeige nach Kategorien'), - URLHelper::getURL('?', ['displaystyle' => 'category']), - Icon::create('assessment') - ); - } - - $sidebar->addWidget($widget, 'ansicht'); - - $actions = new ActionsWidget(); - $actions->addLink( - _('Werkzeugreihenfolge ändern'), - $this->action_url('sorttools'), - Icon::create('arr_2down') - )->asDialog('size=500;reload-on-close'); - - $sidebar->addWidget($actions, 'aktion'); - - - unset($_SESSION['plus']['Kategorielist']); - $plusconfig['course_plus'] = $_SESSION['plus']; - UserConfig::get($GLOBALS['user']->id)->store('PLUS_SETTINGS', $plusconfig); - } - - private function getSortedList(Range $context) - { - - $list = []; - $cat_index = []; - - foreach (PluginEngine::getPlugins('StudipModule') as $plugin) { - if (!$plugin->isActivatableForContext($context)) { - continue; - } - - - - if (!$this->sem_class->isModuleMandatory(get_class($plugin)) - && $this->sem_class->isModuleAllowed(get_class($plugin)) - ) { - - $info = $plugin->getMetadata(); - - $indcat = isset($info['category']) ? $info['category'] : 'Sonstiges'; - if (!array_key_exists($indcat, $cat_index)) { - array_push($cat_index, $indcat); - } - $plugin_id = 'plugin_' . $plugin->getPluginId(); - $tool = ToolActivation::find([$context->getRangeId(), $plugin->getPluginId()]); - $displayname = $info['displayname'] ?? $plugin->getPluginname(); - if ($tool && $tool->metadata['displayname']) { - $displayname .= ' (' .$tool->getDisplayname() . ')'; - } - $visibility = $tool && $tool->metadata['visibility'] ? $tool->metadata['visibility'] : 'autor'; - - if ($_SESSION['plus']['displaystyle'] != 'category') { - - - $list['Funktionen von A-Z'][$plugin_id]['object'] = $plugin; - $list['Funktionen von A-Z'][$plugin_id]['type'] = 'plugin'; - $list['Funktionen von A-Z'][$plugin_id]['moduleclass'] = get_class($plugin); - $list['Funktionen von A-Z'][$plugin_id]['sorter'] = mb_strtolower($displayname); - $list['Funktionen von A-Z'][$plugin_id]['displayname'] = $displayname; - $list['Funktionen von A-Z'][$plugin_id]['visibility'] = $visibility; - } else { - - $cat = isset($info['category']) ? $info['category'] : 'Sonstiges'; - - if (!isset($_SESSION['plus']['Kategorie'][$cat])) { - $_SESSION['plus']['Kategorie'][$cat] = 1; - } - - $list[$cat][$plugin_id]['object'] = $plugin; - $list[$cat][$plugin_id]['moduleclass'] = get_class($plugin); - $list[$cat][$plugin_id]['type'] = 'plugin'; - $list[$cat][$plugin_id]['sorter'] = mb_strtolower($displayname); - $list[$cat][$plugin_id]['displayname'] = $displayname; - $list[$cat][$plugin_id]['visibility'] = $visibility; - } - } - } - - $sortedcats['Lehr- und Lernorganisation'] = []; - $sortedcats['Kommunikation und Zusammenarbeit'] = []; - $sortedcats['Inhalte und Aufgabenstellungen'] = []; - - foreach ($list as $cat_key => $cat_val) { - uasort($cat_val, function ($a, $b) {return strcmp($a['sorter'], $b['sorter']);}); - $list[$cat_key] = $cat_val; - if ($cat_key != 'Sonstiges') { - $sortedcats[$cat_key] = $list[$cat_key]; - } - } - - if (isset($list['Sonstiges'])) { - $sortedcats['Sonstiges'] = $list['Sonstiges']; - } - - - $_SESSION['plus']['Kategorielist'] = array_flip($cat_index); - - return $sortedcats; - } - -} diff --git a/app/views/admin/plugin/edit_description.php b/app/views/admin/plugin/edit_description.php new file mode 100644 index 0000000000000000000000000000000000000000..45a48d133d2504635b8f3cf84647998abc64bdc5 --- /dev/null +++ b/app/views/admin/plugin/edit_description.php @@ -0,0 +1 @@ +<?= $form->render() ?> diff --git a/app/views/admin/plugin/index.php b/app/views/admin/plugin/index.php index 1d51d8de08d1098dae1954590bcc749edc950011..c2a2034af33c707b5a95492ab8515f7046c833f8 100644 --- a/app/views/admin/plugin/index.php +++ b/app/views/admin/plugin/index.php @@ -104,6 +104,16 @@ use Studip\Button, Studip\LinkButton; _('Zugriffsrechte bearbeiten'), Icon::create('edit', 'clickable', ['title' => _('Zugriffsrechte bearbeiten')]) ) ?> + <? + if (in_array('StudipModule', $plugin['type'])) { + $actionMenu->addLink( + $controller->url_for('admin/plugin/edit_description/' . $pluginid), + _('Beschreibung und Hervorhebung'), + Icon::create('infopage', Icon::ROLE_CLICKABLE, ['title' => _('Beschreibung und Hervorhebung')]), + ['data-dialog' => 'size=big'] + ); + } + ?> <? if (!$plugin['depends'] && isset($update_info[$pluginid]['version']) && !$plugin['core']): ?> <? $actionMenu->addLink( diff --git a/app/views/course/contentmodules/index.php b/app/views/course/contentmodules/index.php new file mode 100644 index 0000000000000000000000000000000000000000..af8f3e1ebafee90aec2ff8ae76bdb1dc0b85ce05 --- /dev/null +++ b/app/views/course/contentmodules/index.php @@ -0,0 +1 @@ +<div class="content-modules-vue-app" is="ContentModules"></div> diff --git a/app/views/course/contentmodules/info.php b/app/views/course/contentmodules/info.php new file mode 100644 index 0000000000000000000000000000000000000000..63374e3f35d89f363c51340a4b279c73d81e7ec4 --- /dev/null +++ b/app/views/course/contentmodules/info.php @@ -0,0 +1,56 @@ +<? if ($plugin->getDescriptionMode() === 'replace_all') : ?> + <?= formatReady($plugin->getPluginDescription()) ?> +<? else : ?> + <div class="contentmodule_info"> + <div class="main_part"> + <div class="header"> + <div class="image"> + <? + $icon = $metadata['icon']; + if (!$icon) { + $icon = Icon::create('plugin', Icon::ROLE_INFO); + } + if (!is_a($icon, 'Icon')) { + $icon = Icon::create($icon); + } + ?> + <?= $icon->asImg(100) ?> + </div> + <div class="text"> + <h1><?= htmlReady($metadata['displayname'] ?? $plugin->getPluginName()) ?></h1> + <strong> + <?= htmlReady($metadata['summary']) ?> + </strong> + </div> + </div> + <div class="content-modules-controls-vue-app" is="ContentModulesControl" module_id="<?= htmlReady($plugin->getPluginId()) ?>"></div> + <? $keywords = preg_split( "/;/", $metadata['keywords'], -1, PREG_SPLIT_NO_EMPTY) ?> + <? if (count($keywords)) : ?> + <ul class="keywords"> + <? foreach ($keywords as $keyword) : ?> + <li> + <?= htmlReady($keyword) ?> + </li> + <? endforeach ?> + </ul> + <? endif ?> + <div class="description"> + <?= formatReady($plugin->getPluginDescription()) ?> + </div> + </div> + <? if (isset($metadata['screenshots']) && count($metadata['screenshots']['pictures'])) : ?> + <ul class="screenshots clean"> + <? foreach ($metadata['screenshots']['pictures'] as $pictures) : ?> + <li> + <a href="<?= $plugin->getPluginURL().$metadata['screenshots']['path'].'/'.$pictures['source'] ?>" + data-lightbox="<?= htmlReady($metadata['displayname'] ?? $plugin->getPluginName()) ?>" + data-title="<?= htmlReady($pictures['title']) ?>"> + <img src="<?= $plugin->getPluginURL().$metadata['screenshots']['path'].'/'.$pictures['source'] ?>" alt=""> + <?= htmlReady($pictures['title']) ?> + </a> + </li> + <? endforeach ?> + </ul> + <? endif ?> + </div> +<? endif ?> diff --git a/app/views/course/contentmodules/rename.php b/app/views/course/contentmodules/rename.php new file mode 100644 index 0000000000000000000000000000000000000000..c7f4ef7ccd849adcea956c7ebc843513f88a7816 --- /dev/null +++ b/app/views/course/contentmodules/rename.php @@ -0,0 +1,24 @@ +<form class="default" + action="<?= $controller->link_for('course/contentmodules/rename/' . $module->getPluginId()) ?>" + method="post"> + <fieldset> + + <label> + <?= _('Neuer Name des Werkzeugs') ?> + <input type="text" + name="displayname" + value="<?= $tool && $tool['metadata'] ? htmlReady($tool['metadata']['displayname']) : ''?>" + placeholder="<?= htmlReady($metadata['displayname']) ?>"> + </label> + + <div> + <?= htmlReady(sprintf(_('Ursprünglicher Werkzeugname ist "%s".'), $metadata['displayname'])) ?> + </div> + </fieldset> + <div data-dialog-button> + <?= \Studip\Button::create(_('Speichern'))?> + <? if ($tool && $tool['metadata'] && $tool['metadata']['displayname']) : ?> + <?= \Studip\Button::create(_('Namen löschen'), 'delete') ?> + <? endif ?> + </div> +</form> diff --git a/app/views/course/plus/edittool.php b/app/views/course/plus/edittool.php deleted file mode 100644 index 124d2b45e12a1026ae5ce4b170ddaca4df51e841..0000000000000000000000000000000000000000 --- a/app/views/course/plus/edittool.php +++ /dev/null @@ -1,25 +0,0 @@ -<form class="default" action="<?=$controller->action_link('edittool/plugin_' . $tool->plugin_id)?>" method="post"> - <?= CSRFProtection::tokenTag() ?> - <fieldset> - <label for="displayname"> - <?=_('Name des Werkzeugs')?> - <input type="text" name="displayname" id="displayname" value="<?=htmlReady($tool->getDisplayname())?>"> - </label> - - <label><?=_('Sichtbarkeit')?></label> - <div class="hgroup"> - <label for="permission_autor"> - <?=_('Studierende')?> - <input type="radio" name="permission" id="permission_autor" value="autor" checked> - </label> - <label for="permission_tutor"> - <?=_('Lehrende')?> - <input type="radio" name="permission" id="permission_tutor" value="tutor" <?= $tool->getVisibilityPermission() === 'tutor' ? 'checked' : '' ?>> - </label> - </div> - </fieldset> - - <footer data-dialog-button> - <?= Studip\Button::createAccept(_('Speichern'), 'save') ?> - </footer> -</form> diff --git a/app/views/course/plus/index.php b/app/views/course/plus/index.php deleted file mode 100644 index adf58b2ee1ac45019892edd904bfd2735acabc35..0000000000000000000000000000000000000000 --- a/app/views/course/plus/index.php +++ /dev/null @@ -1,245 +0,0 @@ -<? - -/* - * Copyright (c) 2012 Rasmus Fuhse <fuhse@data-quest.de> - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - */ - -use Studip\Button; - -?> - -<form action="<?= URLHelper::getLink() ?>" method="post" class="default"> - <?= CSRFProtection::tokenTag() ?> - <input name="uebernehmen" value="1" type="hidden"> - <table class="default nohover plus"> - <!-- <caption><?= _("Inhaltselemente") ?></caption> --> - <tbody> - <? foreach ($available_modules as $category => $pluginlist) : ?> - <? - $visibility = ''; - if ($_SESSION['plus']['displaystyle'] != 'category' && $category != 'Funktionen von A-Z') { - $visibility = 'invisible'; - } - if (isset($_SESSION['plus']) && empty($_SESSION['plus']['Kategorie'][$category]) && $category != 'Funktionen von A-Z') { - $visibility = 'invisible'; - } - ?> - <tr class="<?= $visibility; ?>"> - <th colspan=3> - <?= htmlReady($category) ?> - </th> - </tr> - <? foreach ($pluginlist as $key => $val) : ?> - <? - if ($val['type'] == 'plugin') { - $plugin = $val['object']; - $plugin_activated = $plugin->isActivated(); - $info = $plugin->getMetadata(); - - //Checkbox - $anchor = 'p_' . $plugin->getPluginId(); - $cb_disabled = ''; - $cb_checked = $plugin_activated ? "checked" : ""; - - $pluginname = $val['displayname']; - $url = $plugin->isCorePlugin() ? $GLOBALS['ABSOLUTE_URI_STUDIP'] : $plugin->getPluginURL(); - $pluginvisibility = $val['visibility']; - } - ?> - - <tr id="<?= htmlReady($anchor); ?>" - class="<?= $visibility; ?>"> - <td class="element" colspan=3> - - <div class="plus_basic"> - <input type="checkbox" - id="<?= $key ?>" - name="<?= $key ?>" - data-moduleclass="<?= htmlReady($val['moduleclass']) ?>" - data-key="<?= htmlReady($val['modulkey'] ?? '') ?>" - value="TRUE" <?= $cb_disabled ?> <?= $cb_checked ?> - onClick="STUDIP.Plus.setModule.call(this);"> - <div class="element_header"> - <!-- Name --> - <label for="<?= $key ?>"> - <strong><?= htmlReady($pluginname) ?></strong> - <? if ($cb_checked) : ?> - <?= Icon::create( - $pluginvisibility === 'autor' ? 'visibility-visible' : 'visibility-invisible', - Icon::ROLE_INFO, - [ - 'title' => sprintf( - _('%s für Studierende'), - $pluginvisibility === 'autor' ? _('Sichtbar') : _('Unsichtbar') - ) - ] - ) ?> - <? endif ?> - </label> - </div> - <div class="element_description"> - <? if (isset($info['icon'])) : ?> - <? /* TODO: Plugins should use class "Icon" */ ?> - <? if (is_string($info['icon'])) : ?> - <img class="plugin_icon text-bottom" alt="" - src="<?= htmlReady($url . "/" . $info['icon']) ?> "> - <? else: ?> - <?= $info['icon']->asImg(['class' => 'plugin_icon text-bottom', 'alt' => '']) ?> - <? endif ?> - <? endif ?> - <strong class="shortdesc"> - <? if (isset($info['descriptionshort'])) : ?> - <? foreach (explode('\n', $info['descriptionshort']) as $descriptionshort) : ?> - <?= htmlReady($descriptionshort) ?> - <? endforeach ?> - <? endif ?> - <? if (!isset($info['descriptionshort'])) : ?> - <? if (isset($info['summary'])) : ?> - <?= htmlReady($info['summary']) ?> - <? elseif (isset($info['description'])) : ?> - <?= htmlReady($info['description']) ?> - <? else: ?> - <?= _('Keine Beschreibung vorhanden.') ?> - <? endif ?> - <? endif ?> - </strong> - </div> - <? if ($plugin_activated) : ?> - <? - $actionMenu = ActionMenu::get()->setContext($pluginname); - $actionMenu->addLink( - $controller->action_url('edittool/' . $key), - _('Optionen bearbeiten'), - Icon::create('edit'), - ['data-dialog' => 'size=auto'] - ); - if (method_exists($plugin, 'deleteContent')) { - $actionMenu->addLink( - $controller->action_url('index', ['deleteContent' => 1, 'name' => $key]), - _('Inhalte löschen'), - Icon::create('trash') - ); - } - ?> - <div style="float: right"> - <?= $actionMenu->render() ?> - </div> - <? endif ?> - </div> - - <? if ($_SESSION['plus']['View'] === 'openall' || !isset($_SESSION['plus'])) : ?> - <div class="plus_expert hidden-tiny-down"> - <div class="screenshot_holder"> - <? if (isset($info['screenshot']) || isset($info['screenshots'])) : - if (isset($info['screenshots'])) { - $title = $info['screenshots']['pictures'][0]['title']??''; - $source = $info['screenshots']['path'] . '/' . $info['screenshots']['pictures'][0]['source']; - } else { - $fileext = pathinfo($info['screenshot'], PATHINFO_EXTENSION); - $title = str_replace('_', ' ', basename($info['screenshot'], ".$fileext")); - $source = $info['screenshot']; - } - ?> - - <a href="<?= htmlReady("$url/$source") ?>" - data-lightbox="<?= htmlReady($pluginname) ?>" - data-title="<?= htmlReady($title) ?>"> - <img class="big_thumb" src="<?= htmlReady("$url/$source") ?>" - alt="<?= htmlReady($pluginname) ?>"/> - </a> - - <? if (isset($info['additionalscreenshots']) - || (isset($info['screenshots']) && count($info['screenshots']) > 1)) :?> - <div class="thumb_holder"> - <? - if (isset($info['screenshots'])) { - $counter = count($info['screenshots']['pictures']); - $cstart = 1; - } else { - $counter = count($info['additionalscreenshots']); - $cstart = 0; - } - ?> - - <? for ($i = $cstart; $i < $counter; $i++) :?> - <? - if (isset($info['screenshots'])) { - $title = $info['screenshots']['pictures'][$i]['title']?? ''; - $source = $info['screenshots']['path'] . '/' . $info['screenshots']['pictures'][$i]['source']; - } else { - $fileext = pathinfo($info['additionalscreenshots'][$i], PATHINFO_EXTENSION); - $title = str_replace('_', ' ', basename($info['additionalscreenshots'][$i], ".$fileext")); - $source = $info['additionalscreenshots'][$i]; - } - ?> - <a href="<?= htmlReady("$url/$source") ?>" - data-lightbox="<?= htmlReady($pluginname) ?>" - data-title="<?= htmlReady($title) ?>"> - <img class="small_thumb" - src="<?= htmlReady("$url/$source") ?>" - alt="<?= htmlReady($pluginname) ?>"> - </a> - <? endfor ?> - </div> - <? endif ?> - <? endif ?> - </div> - <div class="descriptionbox"> - <? if (isset($info['keywords'])) : ?> - <ul class="keywords"> - <? foreach (explode(';', $info['keywords']) as $keyword) : ?> - <li><?= htmlReady($keyword) ?> </li> - <? endforeach ?> - </ul> - <? endif ?> - <? if (isset($info['descriptionlong'])) : ?> - <? foreach (explode('\n', $info['descriptionlong']) as $descriptionlong) : ?> - <p class="longdesc"> - <?= htmlReady($descriptionlong) ?> - </p> - <? endforeach ?> - <? endif ?> - <? if (!isset($info['descriptionlong']) && isset($info['summary'])) : ?> - <p class="longdesc"> - <? if (isset($info['description'])) : ?> - <?= htmlReady($info['description']) ?> - <? else: ?> - <?= _('Keine Beschreibung vorhanden.') ?> - <? endif ?> - </p> - <? endif ?> - <? if (isset($info['homepage'])) : ?> - <p> - <strong><?= _('Weitere Informationen:') ?></strong> - <a href="<?= htmlReady($info['homepage']) ?>"> - <?= htmlReady($info['homepage']) ?> - </a> - </p> - <? endif ?> - <? if (isset($info['helplink'])) : ?> - <a class="helplink" href=" <?= htmlReady($info['helplink']) ?> "> - ...<?= _('mehr') ?> - </a> - <? endif ?> - </div> - </div> - <? endif ?> - </td> - </tr> - <? endforeach ?> - <? endforeach ?> - </tbody> - <tfoot> - <tr class="hidden-js"> - <td colspan="3"> - <?= Button::create(_('An- / Ausschalten'), 'uebernehmen') ?> - </td> - </tr> - </tfoot> - </table> -</form> diff --git a/app/views/course/plus/sorttools.php b/app/views/course/plus/sorttools.php deleted file mode 100644 index 153e7c9f8ff93bed43265627e27b037f5b027449..0000000000000000000000000000000000000000 --- a/app/views/course/plus/sorttools.php +++ /dev/null @@ -1,15 +0,0 @@ -<section class="contentbox course-statusgroups" data-sortable="<?=$controller->action_link('sorttools', ['order' => 1]) ?>"> -<? if ($sem->tools): ?> - <? foreach ($sem->tools as $tool): ?> - <?php if (!$tool->getStudipModule()) continue; ?> - <article class="draggable" id="plugin_<?= $tool->plugin_id ?>"> - <header> - <span class="drag-handle"></span> - <h1><?= htmlready($tool->getDisplayName()) ?></h1> - </header> - </article> - <? endforeach ?> -<? endif ?> -</section> - - diff --git a/db/migrations/5.4.10_contentmodules_description.php b/db/migrations/5.4.10_contentmodules_description.php new file mode 100644 index 0000000000000000000000000000000000000000..266be87662e95c2129034f15ae9f419feb6fc8c0 --- /dev/null +++ b/db/migrations/5.4.10_contentmodules_description.php @@ -0,0 +1,47 @@ +<?php +final class ContentmodulesDescription extends Migration +{ + public function description() + { + return 'Content modules of a course, institute or studygroup got revamped.'; + } + + protected function up() + { + $query = "ALTER TABLE `plugins` + ADD COLUMN `description` TEXT DEFAULT NULL, + ADD COLUMN `description_mode` ENUM('add', 'override_description', 'replace_all') DEFAULT 'add', + ADD COLUMN `highlight_until` INT(11) UNSIGNED DEFAULT NULL, + ADD COLUMN `highlight_text` VARCHAR(64) DEFAULT NULL, + ADD KEY `highlight_until` (`highlight_until`)"; + DBManager::get()->exec($query); + + $query = "INSERT IGNORE INTO `config` (`field`, `value`, `type`, `range`, `mkdate`, `chdate`, `description`) + VALUES (:name, :value, :type, :range, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description)"; + + $statement = DBManager::get()->prepare($query); + $statement->execute([ + ':name' => 'CONTENTMODULES_TILED_DISPLAY', + ':description' => 'Bevorzugt ein Nutzer eine Kachelansicht auf der Werkzeugseite in den Veranstaltungen oder lieber eine Tabelle?', + ':range' => 'user', + ':type' => 'boolean', + ':value' => '1' + ]); + } + + protected function down() + { + $query = "ALTER TABLE `plugins` + DROP COLUMN `description`, + DROP COLUMN `highlight_until`, + DROP COLUMN `highlight_text`"; + DBManager::get()->exec($query); + + $query = "DELETE FROM `config_values` + WHERE `field` = 'CONTENTMODULES_TILED_DISPLAY' "; + DBManager::get()->exec($query); + $query = "DELETE FROM `config` + WHERE `field` = 'CONTENTMODULES_TILED_DISPLAY' "; + DBManager::get()->exec($query); + } +} diff --git a/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php b/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php index c4fda2f33a1d456d290a26f49f2d633a93865225..c45f3a4aa92126122c0a903877da2936bf22e5f6 100644 --- a/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php +++ b/lib/classes/JsonApi/Routes/ConfigValues/ConfigValuesUpdate.php @@ -33,6 +33,7 @@ class ConfigValuesUpdate extends JsonApiController // TODO: zunächst kann diese Route nur Konfigurationseinstellungen vom Typ bool ändern if ( 'boolean' !== $resource->entry['type'] + && $resource->entry['field'] !== 'CONTENTMODULES_TILED_DISPLAY' && $resource->entry['field'] !== 'MY_COURSES_OPEN_GROUPS' && $resource->entry['field'] !== 'MY_COURSES_VIEW_SETTINGS' ) { diff --git a/lib/classes/JsonApi/Schemas/Course.php b/lib/classes/JsonApi/Schemas/Course.php index acc302ec3d965620f7b440f20bdd6fe9c1ced4e2..09f11754e4016148866e640446d57d5fe06d1867 100644 --- a/lib/classes/JsonApi/Schemas/Course.php +++ b/lib/classes/JsonApi/Schemas/Course.php @@ -27,6 +27,7 @@ class Course extends SchemaProvider const REL_START_SEMESTER = 'start-semester'; const REL_STATUS_GROUPS = 'status-groups'; const REL_WIKI_PAGES = 'wiki-pages'; + const REL_TOOLS = 'tools'; public function getId($course): ?string { @@ -82,6 +83,7 @@ class Course extends SchemaProvider $relationships = $this->getSemTypeRelationship($relationships, $course, $includeList); $relationships = $this->getStatusGroupsRelationship($relationships, $course, $includeList); $relationships = $this->getWikiPagesRelationship($relationships, $course, $includeList); + $relationships = $this->getToolsRelationship($relationships, $course, $includeList); return $relationships; } @@ -298,6 +300,26 @@ class Course extends SchemaProvider return $relationships; } + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function getToolsRelationship( + array $relationships, + \Course $course, + $includeData + ) { + $relation = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($course, self::REL_TOOLS), + ] + ]; + if (in_array(self::REL_TOOLS, $includeData)) { + $relation[self::RELATIONSHIP_DATA] = $course->tools->getArrayCopy(); + } + + return array_merge($relationships, [self::REL_TOOLS => $relation]); + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/lib/classes/SemClass.class.php b/lib/classes/SemClass.class.php index 36bfd70d631562eddc3e28f7396dc9ecec82ba6c..0fd486870dfe7adecf2fc22f5c3f45bafa66567d 100644 --- a/lib/classes/SemClass.class.php +++ b/lib/classes/SemClass.class.php @@ -71,10 +71,10 @@ class SemClass implements ArrayAccess $type = isset($INST_MODULES[$type]) ? $type : 'default'; $data = [ - 'name' => 'Generierte Standardinstitutsklasse', + 'name' => _('Generierte Standardinstitutsklasse'), 'visible' => 1, - 'overview' => 'CoreOverview', // always available - 'admin' => 'CoreAdmin' // always available + 'admin' => 'CoreAdmin', // always available + 'overview' => 'CoreOverview' // always available ]; $slots = [ 'forum' => 'CoreForum', @@ -86,8 +86,8 @@ class SemClass implements ArrayAccess 'personal' => 'CorePersonal' ]; $modules = [ + 'CoreAdmin' => ['activated' => 1, 'sticky' => 1], 'CoreOverview' => ['activated' => 1, 'sticky' => 1], - 'CoreAdmin' => ['activated' => 1, 'sticky' => 1] ]; foreach ($slots as $slot => $module) { diff --git a/lib/classes/forms/DatetimepickerInput.php b/lib/classes/forms/DatetimepickerInput.php index 9bee82bb344cfe53bb769c84170c9ab764468339..060946f687755b48125e5d6257d5620d5247e319 100644 --- a/lib/classes/forms/DatetimepickerInput.php +++ b/lib/classes/forms/DatetimepickerInput.php @@ -22,4 +22,13 @@ class DatetimepickerInput extends Input $template->attributes = $attributes; return $template->render(); } + + /** + * Turns an empty string into null value. + * @return integer|null + */ + public function dataMapper($value) + { + return $value ?: null; + } } diff --git a/lib/classes/forms/Form.php b/lib/classes/forms/Form.php index 936f324b40c36363e41bcd0a9ad448b2d9303353..892e144533f6e7f251f15feb45a0eeda6a5ba5e0 100644 --- a/lib/classes/forms/Form.php +++ b/lib/classes/forms/Form.php @@ -18,6 +18,7 @@ class Form extends Part protected $save_button_name = ''; protected $autoStore = false; + protected $debugmode = false; protected $success_message = ''; protected $collapsable = false; @@ -211,6 +212,17 @@ class Form extends Part return $this; } + public function setDebugMode(bool $debug = true): Form + { + $this->debugmode = $debug; + return $this; + } + + public function getDebugMode(): bool + { + return $this->debugmode; + } + public function getSuccessMessage() : string { return $this->success_message; @@ -301,11 +313,9 @@ class Form extends Part $all_values = []; foreach ($this->getAllInputs() as $input) { $value = $this->getStorableValueFromRequest($input); - if ($value !== null) { - $callback = $this->getStoringCallback($input); - if (is_callable($callback)) { - $stored += $callback($value, $input); - } + $callback = $this->getStoringCallback($input); + if (is_callable($callback)) { + $stored += $callback($value, $input); $all_values[$input->getName()] = $value; } } @@ -388,7 +398,11 @@ class Form extends Part return $input->store; } $context = $input->getParent()->getContextObject(); - if ($context && is_subclass_of($context, \SimpleORMap::class)) { + if ( + $context + && is_subclass_of($context, \SimpleORMap::class) + && $context->isField($input->getName()) + ) { return function ($value) use ($context, $input) { $context[$input->getName()] = $value; }; diff --git a/lib/classes/forms/InfoInput.php b/lib/classes/forms/InfoInput.php new file mode 100644 index 0000000000000000000000000000000000000000..561feeedd9e6e95ced4846fb08183f51359d4b3f --- /dev/null +++ b/lib/classes/forms/InfoInput.php @@ -0,0 +1,20 @@ +<?php + +namespace Studip\Forms; + +class InfoInput extends Input +{ + public function render() + { + $template = $GLOBALS['template_factory']->open('forms/info_input'); + $template->title = $this->title; + $template->value = $this->value; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } + + public function getAllInputNames() + { + return []; + } +} diff --git a/lib/classes/forms/Part.php b/lib/classes/forms/Part.php index 3609eb4fad39be7756c2f501fe5c6a483fba8866..1d1c9d0901fc5ee8b93778338e9d6ed3d58dd144 100644 --- a/lib/classes/forms/Part.php +++ b/lib/classes/forms/Part.php @@ -67,13 +67,13 @@ abstract class Part /** * Adds an Input to this Part. * @param Input $input - * @return Input + * @return $this */ public function addInput(Input $input) { $input->setParent($this); $this->parts[] = $input; - return $input; + return $this; } /** @@ -81,15 +81,15 @@ abstract class Part * * @param string $text The text to be added. * @param bool $text_is_html Whether the text is HTML (true) or plain text (false). Defaults to true. - * @return Text The added text form part. + * @return $this */ - public function addText(string $text, bool $text_is_html = true): Text + public function addText(string $text, bool $text_is_html = true) { $text_part = new Text(); $text_part->setText($text, $text_is_html); $text_part->setParent($this); $this->parts[] = $text_part; - return $text_part; + return $this; } /** @@ -100,16 +100,16 @@ abstract class Part * @param \Icon|null $icon The icon to be used for the link. * @param array $attributes Additional link attributes. * - * @return Link The Text form element containing the link as HTML. + * @return $this */ - public function addLink(string $title, string $url, ?\Icon $icon = null, array $attributes = []): Link + public function addLink(string $title, string $url, ?\Icon $icon = null, array $attributes = []) { $link = new Link($url, $title, $icon); $link->setAttributes($attributes); $this->addPart($link); - return $link; + return $this; } /** diff --git a/lib/classes/forms/WysiwygInput.php b/lib/classes/forms/WysiwygInput.php new file mode 100644 index 0000000000000000000000000000000000000000..f2702261129fc63b46c2fbdf01d7c19acae3fe90 --- /dev/null +++ b/lib/classes/forms/WysiwygInput.php @@ -0,0 +1,30 @@ +<?php + +namespace Studip\Forms; + +class WysiwygInput extends Input +{ + public function render() + { + $template = $GLOBALS['template_factory']->open('forms/wysiwyg_input'); + $template->title = $this->title; + $template->name = $this->name; + $template->value = $this->value; + $template->id = md5(uniqid()); + $template->required = $this->required; + $template->attributes = arrayToHtmlAttributes($this->attributes); + return $template->render(); + } + + public function getRequestValue() + { + $value = \Request::get($this->name); + if (trim($value)) { + return \Studip\Markup::markAsHtml( + \Studip\Markup::purifyHtml($value) + ); + } else { + return ''; + } + } +} diff --git a/lib/classes/sidebar/OptionsWidget.php b/lib/classes/sidebar/OptionsWidget.php index a7931ce57852979e4fff1875e7dbb7d91ec0745b..1e56ed1cd76d54921c9e1e505a0855dfa92af494 100644 --- a/lib/classes/sidebar/OptionsWidget.php +++ b/lib/classes/sidebar/OptionsWidget.php @@ -55,8 +55,9 @@ class OptionsWidget extends ListWidget $url = html_entity_decode($url); $content = sprintf( - '<a href="%s" class="options-radio options-%s" %s>%s</a>', + '<a href="%s" role="radio" aria-checked="%s" class="options-radio options-%s" %s>%s</a>', htmlReady($url), + $checked ? 'true' : 'false', $checked ? 'checked' : 'unchecked', arrayToHtmlAttributes($attributes), htmlReady($label) diff --git a/lib/models/Plugin.php b/lib/models/Plugin.php new file mode 100644 index 0000000000000000000000000000000000000000..2030e8a3c82cb78afc1dc179de5f07c7768f46ac --- /dev/null +++ b/lib/models/Plugin.php @@ -0,0 +1,25 @@ +<?php + +/** + * @property int $id + * @property int $pluginid + * @property string $pluginclassname + * @property string $pluginpath + * @property string $pluginname + * @property string $plugintype + * @property string $enabled + * @property int $navigationpos + * @property int|null $dependentonid + * @property string|null $automatic_update_url + * @property string|null $automatic_update_secret + */ +class Plugin extends SimpleORMap +{ + protected static function configure($config = []) + { + $config['db_table'] = 'plugins'; + $config['i18n_fields'] = ['description', 'highlight_text']; + parent::configure($config); + } + +} diff --git a/lib/models/ToolActivation.php b/lib/models/ToolActivation.php index 422fb0f991d739ffbe302edf62a7ff3aec3b32f7..f3998780656a29346c55c77c3c5d60772ce46b0a 100644 --- a/lib/models/ToolActivation.php +++ b/lib/models/ToolActivation.php @@ -37,6 +37,7 @@ class ToolActivation extends SimpleORMap ]; $config['serialized_fields']['metadata'] = 'JSONArrayObject'; + $config['registered_callbacks']['before_create'][] = 'setMaxPosition'; parent::configure($config); @@ -94,5 +95,4 @@ class ToolActivation extends SimpleORMap return 'nobody'; } } - } diff --git a/lib/modules/Blubber.class.php b/lib/modules/Blubber.class.php index 7b1dffec66aafa1771d389fd39ceef2043c2169b..4dd2f998e297c47fa1646a82a65e208d2559dd81 100644 --- a/lib/modules/Blubber.class.php +++ b/lib/modules/Blubber.class.php @@ -117,12 +117,14 @@ class Blubber extends CorePlugin implements StudipModule public function getMetadata() { return [ - 'summary' => _('Schneller und einfacher Austausch von Informationen in Gesprächsform'), + 'displayname' => _('Blubber'), + 'summary' => _('Schneller Austausch von Informationen in Gesprächsform'), 'description' => _('Blubber ist eine Kommunikationsform mit Ähnlichkeiten zu einem Forum, in dem aber in Echtzeit miteinander kommuniziert werden kann und das durch den etwas informelleren Charakter eher einem Chat anmutet. Anders als im Forum ist es nicht notwendig, die Seiten neu zu laden, um die neuesten Einträge (z. B. Antworten auf eigene Postings) sehen zu können: Die Seite aktualisiert sich selbst bei neuen Einträgen. Dateien (z.B. Fotos, Audiodateien, Links) können per Drag and Drop in das Feld gezogen und somit verlinkt werden. Auch Textformatierungen sind möglich.'), 'descriptionlong' => _('Kommunikationsform mit Ähnlichkeiten zu einem Forum. Im Gegensatz zum Forum kann mit Blubber jedoch in Echtzeit miteinander kommuniziert werden. Das Tool ähnelt durch den etwas informelleren Charakter einem Messenger. Anders als im Forum ist es nicht notwendig, die Seiten neu zu laden, um die neuesten Einträge (z. B. Antworten auf eigene Postings) sehen zu können. Dateien (z. B. Fotos, Audiodateien, Links) können per drag and drop in das Feld gezogen und somit verlinkt werden. Auch Textformatierungen sind möglich.'), 'category' => _('Kommunikation und Zusammenarbeit'), 'keywords' => _('Einfach Text schreiben und mit <Enter> abschicken; Direktes Kontaktieren anderer Stud.IP-NutzerInnen (@Vorname Nachname); Setzen von und Suche nach Stichworten über Hashtags (#Stichwort); Einbinden von Dateien per drag and drop'), 'icon' => Icon::create('blubber', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('blubber', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Blubber', 'pictures' => [ diff --git a/lib/modules/ConsultationModule.class.php b/lib/modules/ConsultationModule.class.php index 56717766df25a260c1dcfb782df4288a21ba6229..c68f8ca2d5819642a1e25b8ba3f0ae578d2aa80a 100644 --- a/lib/modules/ConsultationModule.class.php +++ b/lib/modules/ConsultationModule.class.php @@ -143,6 +143,7 @@ class ConsultationModule extends CorePlugin implements StudipModule, SystemPlugi 'keywords' => _('Terminvergabe, Sprechstunden'), 'displayname' => _('Terminvergabe'), 'icon' => Icon::create('consultation', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('consultation', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Terminvergabe', 'pictures' => [ diff --git a/lib/modules/CoreAdmin.class.php b/lib/modules/CoreAdmin.class.php index ada2af02aa23d38981f75c90c7f2c75e50e0eef1..948809cf51424e9f280cca629770bf699fe7399e 100644 --- a/lib/modules/CoreAdmin.class.php +++ b/lib/modules/CoreAdmin.class.php @@ -24,15 +24,13 @@ class CoreAdmin extends CorePlugin implements StudipModule */ public function getTabNavigation($course_id) { - $sem_create_perm = in_array(Config::get()->SEM_CREATE_PERM, ['root','admin','dozent']) ? Config::get()->SEM_CREATE_PERM : 'dozent'; - if ($GLOBALS['perm']->have_studip_perm('tutor', $course_id)) { $navigation = new Navigation(_('Verwaltung')); $navigation->setImage(Icon::create('admin', Icon::ROLE_INFO_ALT)); $navigation->setActiveImage(Icon::create('admin', Icon::ROLE_INFO)); - $main = new Navigation(_('Verwaltung'), 'dispatch.php/course/management'); - $navigation->addSubNavigation('main', $main); + $main = new Navigation(_('Werkzeuge'), 'dispatch.php/course/contentmodules'); + $navigation->addSubNavigation('contentmodules', $main); if (!Context::isInstitute()) { $item = new Navigation(_('Grunddaten'), 'dispatch.php/course/basicdata/view/' . $course_id); diff --git a/lib/modules/CoreCalendar.class.php b/lib/modules/CoreCalendar.class.php index 78d4b876e200fd0ef29734f455cb3bcddd6cde7b..c0df36736d70a53057fd99c0e33233784921bb4b 100644 --- a/lib/modules/CoreCalendar.class.php +++ b/lib/modules/CoreCalendar.class.php @@ -49,6 +49,7 @@ class CoreCalendar extends CorePlugin implements StudipModule 'summary' => _('Kalender'), 'category' => _('Lehr- und Lernorganisation'), 'icon' => Icon::create('schedule', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('schedule', Icon::ROLE_CLICKABLE), 'displayname' => _('Planer'), ]; } diff --git a/lib/modules/CoreDocuments.class.php b/lib/modules/CoreDocuments.class.php index 4543b8773de18ad60a3888b6e17e7855b2973d66..2acfeef2275a7f27f9614c49c80cef534e29b3a7 100644 --- a/lib/modules/CoreDocuments.class.php +++ b/lib/modules/CoreDocuments.class.php @@ -154,7 +154,7 @@ class CoreDocuments extends CorePlugin implements StudipModule, OERModule public function getMetadata() { return [ - 'summary' => _('Austausch von Dateien'), + 'summary' => _('Austausch von Dateien, Hausaufgabenordner & Terminordner'), 'description' => _('Im Dateibereich können Dateien sowohl von ' . 'Lehrenden als auch von Studierenden hoch- bzw. ' . 'heruntergeladen werden. Es können Ordner angelegt und ' . @@ -183,6 +183,7 @@ class CoreDocuments extends CorePlugin implements StudipModule, OERModule 'können Im Dateibereich bestimmte Rechte (r, w, x, f) für Studierende, wie z.B. das ' . 'Leserecht (r), festgelegt werden.'), 'icon' => Icon::create('files', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('files', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Dateibereich_-_Dateiordnerberechtigung', 'pictures' => [ diff --git a/lib/modules/CoreElearningInterface.class.php b/lib/modules/CoreElearningInterface.class.php index 7e36a1b724342b27e89eb3487e509f1d0b1b98e1..a5f71196a0c8863166ba9d63c4d292b5c2a2959d 100644 --- a/lib/modules/CoreElearningInterface.class.php +++ b/lib/modules/CoreElearningInterface.class.php @@ -120,6 +120,7 @@ class CoreElearningInterface extends CorePlugin implements StudipModule Zugang zu externen Lernplattformen; Aufgaben- und Test-Erstellung'), 'icon' => Icon::create('learnmodule', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('learnmodule', Icon::ROLE_CLICKABLE), 'descriptionshort' => _('Zugang zu extern erstellten Lernmodulen'), 'descriptionlong' => _('Über diese Schnittstelle ist es möglich, Selbstlerneinheiten, '. 'die in externen Programmen erstellt werden, in Stud.IP zur Verfügung '. diff --git a/lib/modules/CoreForum.class.php b/lib/modules/CoreForum.class.php index ba1ee64f7e207349c9ff9984fcbaf7b9daf4fda7..3a43372c2041171a726e350505eb5918e46d724b 100644 --- a/lib/modules/CoreForum.class.php +++ b/lib/modules/CoreForum.class.php @@ -196,6 +196,7 @@ class CoreForum extends CorePlugin implements ForumModule 'category' => _('Kommunikation und Zusammenarbeit'), 'keywords' => _('Möglichkeit zum intensiven, nachhaltigen textbasierten Austausch; (nachträgliche) Strukturierung der Beiträge; Editierfunktion für Lehrende'), 'icon' => Icon::create('forum', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('forum', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Forum', 'pictures' => [ diff --git a/lib/modules/CoreOverview.class.php b/lib/modules/CoreOverview.class.php index 39145aea83b6270e14747f94301436462b3cef55..af1b9604774109bdb10c2a610fb91bba0a819d3e 100644 --- a/lib/modules/CoreOverview.class.php +++ b/lib/modules/CoreOverview.class.php @@ -110,7 +110,10 @@ class CoreOverview extends CorePlugin implements StudipModule public function getMetadata() { return [ - 'displayname' => _('Übersicht') + 'displayname' => _('Übersicht'), + 'summary' => _('Ankündigungen, Termine, Fragebögen & Details'), + 'icon' => Icon::create('home', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('home', Icon::ROLE_CLICKABLE) ]; } diff --git a/lib/modules/CoreParticipants.class.php b/lib/modules/CoreParticipants.class.php index a9fac5c2a4206f6090eeef3a366e3186f6fd7c63..14c885b816bc67366ba4e06f631b4e0c6419434f 100644 --- a/lib/modules/CoreParticipants.class.php +++ b/lib/modules/CoreParticipants.class.php @@ -178,6 +178,7 @@ class CoreParticipants extends CorePlugin implements StudipModule 'bzw. einzelne Teilnehmende separat anzuschreiben.'), 'category' => _('Lehr- und Lernorganisation'), 'icon' => Icon::create('persons', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('persons', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/TeilnehmerInnen', 'pictures' => [ diff --git a/lib/modules/CorePersonal.class.php b/lib/modules/CorePersonal.class.php index 1f99d7f5c2a3f32c424fdc4f97e17575a2e5ba72..71aaa6b728fa5f69aa883b71787c7200629733c4 100644 --- a/lib/modules/CorePersonal.class.php +++ b/lib/modules/CorePersonal.class.php @@ -49,6 +49,7 @@ class CorePersonal extends CorePlugin implements StudipModule 'displayname' => _('MitarbeiterInnen'), 'category' => _('Sonstiges'), 'icon' => Icon::create('persons', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('persons', Icon::ROLE_CLICKABLE) ]; } diff --git a/lib/modules/CoreSchedule.class.php b/lib/modules/CoreSchedule.class.php index 14fab1d1d7c844f40407cd33d19bbde2a5274762..601b618104a8d34274f38c2acad83d8e70aa2841 100644 --- a/lib/modules/CoreSchedule.class.php +++ b/lib/modules/CoreSchedule.class.php @@ -106,6 +106,7 @@ class CoreSchedule extends CorePlugin implements StudipModule 'inhaltlichen Einstimmung der Studierenden können Lehrende den Terminen ' . 'Themen hinzufügen, die z. B. eine Kurzbeschreibung der Inhalte darstellen.'), 'icon' => Icon::create('schedule', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('schedule', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Ablaufplan', 'pictures' => [ diff --git a/lib/modules/CoreScm.class.php b/lib/modules/CoreScm.class.php index 86e10e14d709af591d085ac1f4a6f5c1a278cf61..d37023b96e6ad8739880da6e36a71fe777cc3381 100644 --- a/lib/modules/CoreScm.class.php +++ b/lib/modules/CoreScm.class.php @@ -139,6 +139,7 @@ class CoreScm extends CorePlugin implements StudipModule 'Literatur. Sie kann aber auch für andere beliebige Zusatzinformationen (Links, Protokolle '. 'etc.) verwendet werden.'), 'icon' => Icon::create('infopage', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('infopage', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Freie_Informationsseite', 'pictures' => [ diff --git a/lib/modules/CoreStudygroupAdmin.class.php b/lib/modules/CoreStudygroupAdmin.class.php index 7c67a26f1c42e8ef0c719f4ae97f08a6c02f81f4..d31139066f6af5ee22976644edb957830040b140 100644 --- a/lib/modules/CoreStudygroupAdmin.class.php +++ b/lib/modules/CoreStudygroupAdmin.class.php @@ -35,6 +35,7 @@ class CoreStudygroupAdmin extends CorePlugin implements StudipModule $navigation->setImage(Icon::create('admin', Icon::ROLE_INFO_ALT)); $navigation->setActiveImage(Icon::create('admin', Icon::ROLE_INFO)); + $navigation->addSubNavigation('contentmodules', new Navigation(_('Werkzeuge'), "dispatch.php/course/contentmodules?cid={$course_id}")); $navigation->addSubNavigation('main', new Navigation(_('Verwaltung'), "dispatch.php/course/studygroup/edit/?cid={$course_id}")); $navigation->addSubNavigation('avatar', new Navigation(_('Infobild'), "dispatch.php/avatar/update/course/{$course_id}?cid={$course_id}")); diff --git a/lib/modules/CoreWiki.class.php b/lib/modules/CoreWiki.class.php index 0034098eef481409e8bfc7c682ab03a7c6887b74..4700334d1661b0c80ed1fbd3fa4e26119f9d53ef 100644 --- a/lib/modules/CoreWiki.class.php +++ b/lib/modules/CoreWiki.class.php @@ -119,7 +119,7 @@ class CoreWiki extends CorePlugin implements StudipModule public function getMetadata() { return [ - 'summary' => _('Gemeinsames asynchrones Erstellen und Bearbeiten von Texten'), + 'summary' => _('Gemeinsames Erstellen und Bearbeiten von Texten'), 'description' => _('Im Wiki-Web oder kurz "Wiki" können '. 'verschiedene Autor/-innen gemeinsam Texte, Konzepte und andere '. 'schriftliche Arbeiten erstellen und gestalten, dies '. @@ -151,6 +151,7 @@ class CoreWiki extends CorePlugin implements StudipModule 'PDF-Datei ist integriert.'), 'category' => _('Kommunikation und Zusammenarbeit'), 'icon' => Icon::create('wiki', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('wiki', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Wiki-Web', 'pictures' => [ diff --git a/lib/modules/CoursewareModule.class.php b/lib/modules/CoursewareModule.class.php index d085de22eb6af4a08ed5c2f3f2a9cea0778499e7..9de221f0c22e30bfee41d9d5d3b7e844e836bf1c 100644 --- a/lib/modules/CoursewareModule.class.php +++ b/lib/modules/CoursewareModule.class.php @@ -81,11 +81,11 @@ class CoursewareModule extends CorePlugin implements SystemPlugin, StudipModule public function getIconNavigation($courseId, $last_visit, $user_id) { $statement = DBManager::get()->prepare(" - SELECT COUNT(DISTINCT elem.id) - FROM `cw_structural_elements` AS elem + SELECT COUNT(DISTINCT elem.id) + FROM `cw_structural_elements` AS elem INNER JOIN `cw_containers` as container ON (elem.id = container.structural_element_id) INNER JOIN `cw_blocks` as blocks ON (container.id = blocks.container_id) - WHERE elem.range_type = 'course' + WHERE elem.range_type = 'course' AND elem.range_id = :range_id AND blocks.payload != '' AND blocks.chdate > :last_visit @@ -141,6 +141,7 @@ class CoursewareModule extends CorePlugin implements SystemPlugin, StudipModule 'displayname' => _('Courseware'), 'category' => _('Lehr- und Lernorganisation'), 'icon' => Icon::create('courseware', 'info'), + 'icon_clickable' => Icon::create('courseware', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Courseware', 'pictures' => [ diff --git a/lib/modules/FeedbackModule.class.php b/lib/modules/FeedbackModule.class.php index c369ea72a31160c014e2054a25d13cd37bb3830a..8674f62687b4dde2d93dcb74b810aa2dd682bdb4 100644 --- a/lib/modules/FeedbackModule.class.php +++ b/lib/modules/FeedbackModule.class.php @@ -53,6 +53,7 @@ class FeedbackModule extends CorePlugin implements StudipModule, SystemPlugin 'category' => _('Kommunikation und Zusammenarbeit'), 'keywords' => _('Anlegen von Feedback-Elementen an verschiedenen Stellen; Auswahl verschiedener Feedback-Modi, wie Sternbewertung; Übersicht über alle Feedback-Elemente einer Veranstaltung'), 'icon' => Icon::create('star', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('star', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Feedback', 'pictures' => [ diff --git a/lib/modules/GradebookModule.class.php b/lib/modules/GradebookModule.class.php index ac8d69b2a4a26b9538fad2057f8cecfe9e99fa0a..f459f106c4e9d5f017f8062cc6f007c3f8c2468b 100644 --- a/lib/modules/GradebookModule.class.php +++ b/lib/modules/GradebookModule.class.php @@ -147,6 +147,7 @@ class GradebookModule extends CorePlugin implements SystemPlugin, StudipModule 'category' => _('Lehr- und Lernorganisation'), 'keywords' => _('automatische und manuelle Erfassung von gewichteten Leistungen;Export von Leistungen;persönliche Fortschrittskontrolle'), 'icon' => Icon::create('assessment', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('assessment', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Gradebook', 'pictures' => [ diff --git a/lib/modules/IliasInterfaceModule.class.php b/lib/modules/IliasInterfaceModule.class.php index f5bc83f855dc79ef0dae352481afc879a5e6d097..a8cafbf24d9b7c5a2d2a9a969f79512f5af16206 100644 --- a/lib/modules/IliasInterfaceModule.class.php +++ b/lib/modules/IliasInterfaceModule.class.php @@ -153,6 +153,7 @@ class IliasInterfaceModule extends CorePlugin implements StudipModule, SystemPlu Zugang zu ILIAS; Aufgaben- und Test-Erstellung'), 'icon' => Icon::create('learnmodule', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('learnmodule', Icon::ROLE_CLICKABLE), 'descriptionshort' => _('Zugang zu extern erstellten ILIAS-Lernobjekten'), 'descriptionlong' => _('Über diese Schnittstelle ist es möglich, Lernobjekte aus ' . 'einer ILIAS-Installation (> 5.3.8) in Stud.IP zur Verfügung ' . diff --git a/lib/modules/LtiToolModule.class.php b/lib/modules/LtiToolModule.class.php index 0383f115f640807a749030571daadbdd0c0404da..eac2768184b099be8f6a5119c29147366e924b15 100644 --- a/lib/modules/LtiToolModule.class.php +++ b/lib/modules/LtiToolModule.class.php @@ -108,6 +108,7 @@ class LtiToolModule extends CorePlugin implements StudipModule, SystemPlugin, Pr 'category' => _('Kommunikation und Zusammenarbeit'), 'keywords' => _('Einbindung von LTI-Tools (Version 1.x)'), 'icon' => Icon::create('link-extern', Icon::ROLE_INFO), + 'icon_clickable' => Icon::create('link-extern', Icon::ROLE_CLICKABLE), 'screenshots' => [ 'path' => 'assets/images/plus/screenshots/Lti', 'pictures' => [ diff --git a/lib/navigation/CourseNavigation.php b/lib/navigation/CourseNavigation.php index 2ff62ad8058c9035f931dd344cceeb11324acf98..6e37cf589327b7922d155c04083c762efbb00a6d 100644 --- a/lib/navigation/CourseNavigation.php +++ b/lib/navigation/CourseNavigation.php @@ -53,7 +53,26 @@ class CourseNavigation extends Navigation return; } - foreach ($context->tools as $tool) { + $admin_plugin_ids = []; + $core_admin = PluginManager::getInstance()->getPlugin('CoreAdmin'); + if ($core_admin) { + $admin_plugin_ids[] = $core_admin->getPluginId(); + } + $core_studygroup_admin = PluginManager::getInstance()->getPlugin('CoreStudygroupAdmin'); + if ($core_studygroup_admin) { + $admin_plugin_ids[] = $core_studygroup_admin->getPluginId(); + } + $tools = $context->tools->getArrayCopy(); + usort($tools, function ($a, $b) use ($admin_plugin_ids) { + if (in_array($a['plugin_id'], $admin_plugin_ids)) { + return -1; + } + if (in_array($b['plugin_id'], $admin_plugin_ids)) { + return 1; + } + return $a['position'] - $b['position']; + }); + foreach ($tools as $tool) { if (Context::isInstitute() || Seminar_Perm::get()->have_studip_perm($tool->getVisibilityPermission(), $context->id)) { $studip_module = $tool->getStudipModule(); if ($studip_module instanceof StudipModule) { diff --git a/lib/plugins/core/CorePlugin.php b/lib/plugins/core/CorePlugin.php index e16b77a8e37d653d36c30b534d035e721eb641ec..0bba2897b13a0c6310ea9d5ddd4a3838111bc884 100644 --- a/lib/plugins/core/CorePlugin.php +++ b/lib/plugins/core/CorePlugin.php @@ -60,6 +60,41 @@ abstract class CorePlugin return ''; } + public function getPluginDescription() + { + $metadata = $this->getMetadata(); + $language = getUserLanguage(User::findCurrent()->id); + if ($metadata['descriptionlong_' . $language]) { + return $metadata['descriptionlong_' . $language]; + } + if ($metadata['description_' . $language]) { + return $metadata['description_' . $language]; + } + $description = $metadata['descriptionlong'] ?? $metadata['description']; + + if ($this->plugin_info['description_mode'] === 'override_description') { + return $this->plugin_info['description']; + } else { + return '<!-- HTML --><div>' . $description . '</div>' . $this->plugin_info['description']; + } + } + + public function getDescriptionMode() + { + return $this->plugin_info['description_mode']; + } + + public function isHighlighted() + { + return $this->plugin_info['highlight_until'] > time(); + } + + public function getHighlightText() + { + return $this->plugin_info['highlight_text']; + } + + /** * Checks if the plugin is a core-plugin. Returns true if this is the case. * diff --git a/lib/plugins/core/StudIPPlugin.class.php b/lib/plugins/core/StudIPPlugin.class.php index fafd583f68cc2fbbd9dc138ba1b2ff0332b5cca5..a3dacb6b8b098ddc00af76f2fb0f69c9d8fc3473 100644 --- a/lib/plugins/core/StudIPPlugin.class.php +++ b/lib/plugins/core/StudIPPlugin.class.php @@ -80,6 +80,45 @@ abstract class StudIPPlugin return $this->manifest; } + /** + * Returns the description of the plugin either from plugins-table or from the manifest's descriptionlong + * or description attribute. + * @return string|null + */ + public function getPluginDescription() + { + $metadata = $this->getMetadata(); + $language = getUserLanguage(User::findCurrent()->id); + if ($metadata['descriptionlong_' . $language]) { + return $metadata['descriptionlong_' . $language]; + } + if ($metadata['description_' . $language]) { + return $metadata['description_' . $language]; + } + $description = $metadata['descriptionlong'] ?? $metadata['description']; + + if ($this->plugin_info['description_mode'] === 'override_description') { + return $this->plugin_info['description']; + } else { + return $description . $this->plugin_info['description']; + } + } + + public function getDescriptionMode() + { + return $this->plugin_info['description_mode']; + } + + public function isHighlighted() + { + return $this->plugin_info['highlight_until'] > time(); + } + + public function getHighlightText() + { + return $this->plugin_info['highlight_text']; + } + /** * Returns the version of this plugin as defined in manifest. * @return string diff --git a/lib/plugins/engine/PluginManager.class.php b/lib/plugins/engine/PluginManager.class.php index 86305a188c733ae718dc31c5c95ab774b5316c1b..7fb69bc8e3fac649b0425845144b0f4ec80e3ef2 100644 --- a/lib/plugins/engine/PluginManager.class.php +++ b/lib/plugins/engine/PluginManager.class.php @@ -85,7 +85,11 @@ class PluginManager 'depends' => (int) $plugin['dependentonid'], 'core' => $plugin['pluginpath'] === '', 'automatic_update_url' => $plugin['automatic_update_url'], - 'automatic_update_secret' => $plugin['automatic_update_secret'] + 'automatic_update_secret' => $plugin['automatic_update_secret'], + 'description' => $plugin['description'], + 'description_mode' => $plugin['description_mode'], + 'highlight_until' => $plugin['highlight_until'], + 'highlight_text' => $plugin['highlight_text'] ]; } } @@ -251,11 +255,16 @@ class PluginManager $activation->range_type = $range->getRangeType(); } $plugin = $this->getPluginById($id); + if ($active) { call_user_func([get_class($plugin), 'onActivation'], $id, $rangeId); + StudipLog::log('PLUGIN_ENABLE', $rangeId, $id, User::findCurrent()->id); + NotificationCenter::postNotification('PluginDidActivate', $rangeId, $id); return $activation->store(); } else { call_user_func([get_class($plugin), 'onDeactivation'], $id, $rangeId); + StudipLog::log('PLUGIN_DISABLE', $rangeId, $id, User::findCurrent()->id); + NotificationCenter::postNotification('PluginDidDeactivate', $rangeId, $id); return $activation->delete(); } } diff --git a/lib/seminar_open.php b/lib/seminar_open.php index a381d58d1d1220c7cb7079ce8167f42c95cf2f26..33436d490d2c201f45b87fded6e0420a41ecf082 100644 --- a/lib/seminar_open.php +++ b/lib/seminar_open.php @@ -143,19 +143,6 @@ if (Request::int('disable_plugins') !== null && ($user->id === 'nobody' || $perm // load the default set of plugins PluginEngine::loadPlugins(); -// add navigation item: add modules -if (Context::isCourse() && $perm->have_studip_perm('tutor', Context::getId())) { - $plus_nav = new Navigation(_('Mehr …'), 'dispatch.php/course/plus/index'); - $plus_nav->setDescription(_("Mehr Stud.IP-Funktionen für Ihre Veranstaltung")); - Navigation::addItem('/course/modules', $plus_nav); -} - -// add navigation item: add modules (institute) -if (Context::isInstitute() && $perm->have_studip_perm('admin', Context::getId())) { - $plus_nav = new Navigation(_('Mehr …'), 'dispatch.php/course/plus/index'); - $plus_nav->setDescription(_("Mehr Stud.IP-Funktionen für Ihre Einrichtung")); - Navigation::addItem('/course/modules', $plus_nav); -} // add navigation item for profile: add modules if (Navigation::hasItem('/profile/edit')) { $plus_nav = new Navigation(_('Mehr …'), 'dispatch.php/profilemodules/index'); diff --git a/resources/assets/javascripts/bootstrap/contentmodules.js b/resources/assets/javascripts/bootstrap/contentmodules.js new file mode 100644 index 0000000000000000000000000000000000000000..3d3f886ff0c3d9986aec1634f4654ac77157e069 --- /dev/null +++ b/resources/assets/javascripts/bootstrap/contentmodules.js @@ -0,0 +1,40 @@ +STUDIP.domReady(() => { + const node = document.querySelector('.content-modules-vue-app'); + if (!node) { + return; + } + + Promise.all([ + STUDIP.Vue.load(), + import('../../../vue/store/ContentModulesStore.js').then((config) => config.default), + import('../../../vue/components/ContentModules.vue').then((component) => component.default), + ]).then(([{ createApp, store }, storeConfig, ContentModules]) => { + store.registerModule('contentmodules', storeConfig); + + Object.entries(window.ContentModulesStoreData ?? {}).forEach(([key, value]) => { + store.commit(`contentmodules/${key}`, value); + }); + + const vm = createApp({ + components: { ContentModules } + }); + vm.$mount(node); + }); +}); + +STUDIP.dialogReady(() => { + const node = document.querySelector('.content-modules-controls-vue-app'); + if (!node) { + return; + } + + Promise.all([ + STUDIP.Vue.load(), + import('../../../vue/components/ContentModulesControl.vue').then((component) => component.default), + ]).then(([{ createApp }, ContentModulesControl]) => { + const vm = createApp({ + components: { ContentModulesControl } + }); + vm.$mount(node); + }); +}); diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js index 3d9be08dc9083d848787b5c9e70ac03a60cdadb5..1a6bc53e152950d2b2c3c24e331f05db0e8cfe90 100644 --- a/resources/assets/javascripts/bootstrap/forms.js +++ b/resources/assets/javascripts/bootstrap/forms.js @@ -251,6 +251,8 @@ STUDIP.ready(function () { params.STUDIPFORM_VALIDATIONNOTES = []; params.STUDIPFORM_AUTOSAVEURL = f.dataset.autosave; params.STUDIPFORM_REDIRECTURL = f.dataset.url; + params.STUDIPFORM_SELECTEDLANGUAGES = {}; + params.STUDIPFORM_DEBUGMODE = JSON.parse(f.dataset.debugmode); return params; }, methods: { @@ -278,8 +280,8 @@ STUDIP.ready(function () { data: params, type: 'post', success() { - if (v.STUDIPFORM_REDIRECTURL) { - window.location.href = v.STUDIPFORM_REDIRECTURL + if (v.STUDIPFORM_REDIRECTURL && !v.STUDIPFORM_DEBUGMODE) { + window.location.href = v.STUDIPFORM_REDIRECTURL; } } }); @@ -336,6 +338,13 @@ STUDIP.ready(function () { this[key] = value; } } + }, + selectLanguage(input_name, language_id) { + let languages = { + ...this.STUDIPFORM_SELECTEDLANGUAGES + }; + languages[input_name] = language_id; + this.STUDIPFORM_SELECTEDLANGUAGES = languages; } }, mounted () { diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 915d960659d543eacff381edcc0ea9def9a0641d..9fcb57ba97d4533387744bf94b46f3f8a3b87348 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -82,6 +82,7 @@ import "./bootstrap/admin-courses.js" import "./bootstrap/cache-admin.js" import "./bootstrap/oer.js" import "./bootstrap/courseware.js" +import "./bootstrap/contentmodules.js" import "./bootstrap/responsive-navigation.js" import "./bootstrap/treeview.js" import "./bootstrap/stock-images.js" diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index 55276026788cc9d5629de4eca1ec8611f85b03bd..8ea0a27eed6c0580916c7916073c0ceb7405f046 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -54,7 +54,6 @@ import Overlay from './lib/overlay.js'; import PageLayout from './lib/page_layout.js'; import parseOptions from './lib/parse_options.js'; import PersonalNotifications from './lib/personal_notifications.js'; -import Plus from './lib/plus.js'; import QRCode from './lib/qr_code.js'; import Questionnaire from './lib/questionnaire.js'; import QuickSearch from './lib/quick_search.js'; @@ -145,7 +144,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, { PageLayout, parseOptions, PersonalNotifications, - Plus, QRCode, Questionnaire, QuickSearch, diff --git a/resources/assets/javascripts/lib/plus.js b/resources/assets/javascripts/lib/plus.js deleted file mode 100644 index 0d447fd413fae4e39e09c34f25f77a54edcdd73e..0000000000000000000000000000000000000000 --- a/resources/assets/javascripts/lib/plus.js +++ /dev/null @@ -1,23 +0,0 @@ -const Plus = { - setModule: function () { - $.ajax({ - "url": STUDIP.URLHelper.getURL("dispatch.php/course/plus/trigger"), - "data": { - "moduleclass": $(this).data("moduleclass"), - "key": $(this).data("key"), - "active": $(this).is(":checked") ? 1 : 0 - }, - "dataType": "json", - "type": "post", - "success": function (output) { - if (output.tabs) { - $(".tabs_wrapper").replaceWith(output.tabs); - } - } - }); - } -}; - - - -export default Plus; diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss index d1336153c095db6224b033d0639ddc1a7b49d36f..fe376dbbb163cb8e54d4fdca234a929615363b7e 100644 --- a/resources/assets/stylesheets/scss/forms.scss +++ b/resources/assets/stylesheets/scss/forms.scss @@ -324,6 +324,7 @@ form.default { > * { box-sizing: border-box; flex: 1 0 auto; + max-width: 400px; &:not(:first-child) { margin-left: 3px; @@ -596,3 +597,5 @@ form.inline { } } } + + diff --git a/resources/assets/stylesheets/scss/plus.scss b/resources/assets/stylesheets/scss/plus.scss deleted file mode 100644 index ea0b7b35f14da566e3f80cbc4af505f63b21ae11..0000000000000000000000000000000000000000 --- a/resources/assets/stylesheets/scss/plus.scss +++ /dev/null @@ -1,79 +0,0 @@ -.plus { - .element_header { - display: inline-block; - width: 250px; - margin-left: 5px; - } - - .element_description { - display: inline-block; - margin-left: 20px; - } - - .plugin_icon { - width: 16px; - height: 16px; - } - - .shortdesc { - margin-left: 3px; - } - - .plus_expert { - margin-left: 20px; - width: 97%; - - display: flex; - flex-wrap: wrap; - } - - .screenshot_holder { - width: 250px; - flex: 0 250px; - margin-right: 5mm; - box-sizing: border-box; - } - - .big_thumb { - max-width: 250px; - max-height: 250px; - padding-top: 5mm; - } - - .small_thumb { - margin-left: 2px; - margin-top: 5px; - max-height: 25px; - } - - .thumb_holder { - width: 250px; - text-align: center; - background-color: $content-color-20; - border-top: 1px solid mix($brand-color-lighter, $white, 80%); - border-bottom: 1px solid mix($brand-color-lighter, $white, 80%); - } - - .descriptionbox { - flex: 1 305px; - max-width: 45em; - } - - .keywords { - padding: 5mm; - left: 5mm; - position: relative; - } - - .longdesc { - overflow: hidden; - } - - .helplink { - float: right; - } - - article.studip > section:not(:last-child) { - border-bottom: 1px solid $table-header-color; - } -} diff --git a/resources/assets/stylesheets/studip.scss b/resources/assets/stylesheets/studip.scss index be72f0ab70b1977934e53f19588f34fd452cc8a6..edbc2e12fe92ac8603f96776e1bb373f0e3e6d6c 100644 --- a/resources/assets/stylesheets/studip.scss +++ b/resources/assets/stylesheets/studip.scss @@ -70,7 +70,6 @@ @import "scss/pagination"; @import "scss/personal-notifications"; @import "scss/plugins"; -@import "scss/plus"; @import "scss/progress_indicator.scss"; @import "scss/profile"; @import "scss/qrcode"; diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js index ccff7c5652e0f781903e72e110d8093b909de59f..b8cf935e011761cc70d43b7fe7c5df31394bb7f8 100644 --- a/resources/vue/base-components.js +++ b/resources/vue/base-components.js @@ -13,6 +13,7 @@ import RangeInput from './components/RangeInput.vue'; import Datetimepicker from './components/Datetimepicker.vue'; import TextareaWithToolbar from './components/TextareaWithToolbar.vue'; import I18nTextarea from "./components/I18nTextarea.vue"; +import StudipWysiwyg from "./components/StudipWysiwyg.vue"; // import StudipLoadingIndicator from './StudipLoadingIndicator.vue'; import StudipMessageBox from './components/StudipMessageBox.vue'; import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue'; @@ -36,6 +37,7 @@ const BaseComponents = { StudipFolderSize, StudipIcon, I18nTextarea, + StudipWysiwyg, // StudipLoadingIndicator, StudipMessageBox, StudipProxyCheckbox, diff --git a/resources/vue/components/ContentModules.vue b/resources/vue/components/ContentModules.vue new file mode 100644 index 0000000000000000000000000000000000000000..0a9d54bee0779b526cdcf0bf949fe292f2f4fc67 --- /dev/null +++ b/resources/vue/components/ContentModules.vue @@ -0,0 +1,218 @@ +<template> + <form id="module-list" :class="{'table-display': !isTilesDisplay }"> + <div class="infopanel" v-if="highlighted.length > 0"> + <div class="top"></div> + <div class="navigation_wrapper"> + <a href="#" + v-if="highlightIndex > 0" + @click.prevent="highlightIndex--" + title="$gettext('Vorheriges Inhaltselement')"> + <studip-icon shape="arr_1left" :size="20"></studip-icon> + </a> + <div v-else></div> + + <a :href="getDescriptionURL(highlightedModule)" data-dialog v-cloak class="contentmodule"> + <div class="iconwrapper"> + <img :src="highlightedModule.icon" width="60" height="60" v-cloak> + </div> + <div class="text"> + <div class="title" v-cloak>{{ highlightedModule.toolname }}</div> + <div v-if="highlightedModule.highlight_text" v-cloak> + {{ highlightedModule.highlight_text }} + </div> + </div> + </a> + + <a href="#" + @click.prevent="highlightIndex++" + v-if="highlightIndex < highlighted.length - 1" + :title="$gettext('Nächstes Inhaltselement')"> + <studip-icon shape="arr_1right" :size="20"></studip-icon> + </a> + <div v-else></div> + </div> + </div> + + <component :is="displayComponent" + :modules="modules" + :filtercategory="filterCategory" + ></component> + + <MountingPortal mount-to="#tool-view-switch .sidebar-widget-content .widget-list" name="sidebar-switch"> + <ul class="widget-list widget-links sidebar-views"> + <li :class="{ active: view === 'tiles' }"> + <a href="#" @click.prevent="changeView('tiles')"> + {{ $gettext('Kachelansicht') }} + </a> + </li> + <li :class="{ active: view === 'table' }"> + <a href="#" @click.prevent="changeView('table')"> + {{ $gettext('Tabellarische Ansicht') }} + </a> + </li> + </ul> + </MountingPortal> + + <MountingPortal mount-to="#tool-filter-category .sidebar-widget-content .widget-list" name="sidebar-filter"> + <ul class="widget-list widget-options"> + <li> + <a class="options-radio" + :class="filterCategory === null ? 'options-checked' : 'options-unchecked'" + role="radio" + :aria-checked="filterCategory === null ? 'true' : 'false'" + href="#" + @click.prevent="setFilterCategory(null)" + > + {{ $gettext('Alle Kategorien') }} + </a> + </li> + <li v-for="category in categories" :key="category"> + <a class="options-radio" + :class="filterCategory === category ? 'options-checked' : 'options-unchecked'" + href="#" + role="radio" + :aria-checked="filterCategory === category ? 'true' : 'false'" + @click.prevent="setFilterCategory(category)" + > + {{ category }} + </a> + </li> + </ul> + </MountingPortal> + </form> +</template> +<script> +import ContentModulesEditTable from './ContentmodulesEditTable.vue'; +import ContentModulesEditTiles from './ContentModulesEditTiles.vue'; +import ContentModulesMixin from '../mixins/ContentModulesMixin.js'; +import { mapMutations, mapState } from 'vuex'; + +export default { + name: 'ContentModules', + mixins: [ContentModulesMixin], + data: () => ({ + highlightIndex: 0, + }), + computed: { + ...mapState('contentmodules', [ + 'highlighted', + ]), + isTilesDisplay() { + return this.view === 'tiles'; + }, + displayComponent() { + return this.isTilesDisplay ? ContentModulesEditTiles : ContentModulesEditTable; + }, + highlightedModule() { + const id = this.highlighted[this.highlightIndex]; + return this.$store.getters['contentmodules/getModuleById'](id); + }, + }, + methods: { + ...mapMutations('contentmodules', [ + 'setFilterCategory', + ]), + }, +}; +</script> +<style lang="scss"> +.admin_contentmodules { + .drag-handle { + display: inline-block; + + width: 6px; + height: 20px; + margin-top: 5px; + + background-size: auto 20px; + } +} + +.admin_contentmodules-move, /* apply transition to moving elements */ +.admin_contentmodules-enter-active, +.admin_contentmodules-leave-active { +} + +.admin_contentmodules-enter-from, +.admin_contentmodules-leave-to { + opacity: 0; + transform: translateX(30px) translateY(30px); +} + +/* ensure leaving items are taken out of layout flow so that moving + animations can be calculated correctly. */ +.admin_contentmodules-leave-active { + position: absolute; +} +</style> +<style lang="scss" scoped> +.infopanel { + padding: 10px; + background-color: var(--content-color-20); + width: 840px; + max-width: 100%; + box-sizing: border-box; + height: 200px; + max-height: 200px; + overflow: hidden; + text-align: center; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + margin-bottom: 10px; + + .table-display & { + width: unset; + } + + > .top { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + > h2 { + font-weight: normal; + margin-top: 5px; + } + } + + > .navigation_wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + > * { + min-width: 20px; + min-height: 20px; + } + > .contentmodule { + display: flex; + flex-direction: row; + .iconwrapper { + background-color: white; + border-radius: 50px; + display: flex; + align-items: center; + justify-content: center; + width: 90px; + height: 90px; + margin-right: 20px; + } + .title { + margin-top: 10px; + font-size: 1.3em; + font-weight: bold; + } + } + } + + + .back-button { + float: left; + position: relative; + top: 20px; + } +} +</style> diff --git a/resources/vue/components/ContentModulesControl.vue b/resources/vue/components/ContentModulesControl.vue new file mode 100644 index 0000000000000000000000000000000000000000..0bfcc0d7e5d0779a0d78ed093fe43000ce432a11 --- /dev/null +++ b/resources/vue/components/ContentModulesControl.vue @@ -0,0 +1,91 @@ +<template> + <div class="controls"> + <div> + <label v-if="!module.mandatory"> + <input type="checkbox" :checked="module.active" @click="toggleModuleActivation(module)" :ref="'checkbox_' + module.id"> + {{ module.active ? $gettext('Werkzeug ist aktiv') : $gettext('Werkzeug ist inaktiv') }} + </label> + </div> + <div> + <a href="#" + class="toggle_visibility" + role="checkbox" + v-if="module.active && !module.mandatory" + :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'" + @click.prevent="toggleModuleVisibility(module)"> + <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'" + class="text-bottom" + :title="$gettextInterpolate($gettext('Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname})"></studip-icon> + </a> + </div> + </div> +</template> +<script> +import ContentModulesMixin from '../mixins/ContentModulesMixin.js'; + +export default { + name: 'ContentModulesControl', + props: { + module_id: { + type: String, + required: true + } + }, + mixins: [ContentModulesMixin], + computed: { + module () { + return this.modules.find(m => m.id == this.module_id) ?? null; + } + } +}; +</script> +<style lang="scss"> +.contentmodule_info { + display: flex; + > .main_part { + > .header { + display: flex; + align-items: center; + > .image { + width: 200px; + height: 150px; + display: flex; + justify-content: center; + align-items: center; + } + > .text { + display: flex; + flex-direction: column; + } + + } + > .controls { + background-color: var(--content-color-20); + padding: 5px; + display: flex; + justify-content: space-between; + } + > .keywords { + margin-top: 10px; + margin-bottom: 10px; + padding-left: 25px; + } + > .description { + margin-top: 10px; + } + } + > .screenshots { + margin-left: 10px; + max-width: 270px; + > li { + margin-top: 20px; + margin-bottom: 20px; + img { + display: block; + width: 100%; + } + } + + } +} +</style> diff --git a/resources/vue/components/ContentModulesEditTiles.vue b/resources/vue/components/ContentModulesEditTiles.vue new file mode 100644 index 0000000000000000000000000000000000000000..2edd43939683e3d5598cfac1ba08e7d0b4f8b9c5 --- /dev/null +++ b/resources/vue/components/ContentModulesEditTiles.vue @@ -0,0 +1,165 @@ +<template> + <draggable v-model="sortedModules" handle=".dragarea"> + <transition-group name="admin_contentmodules" + class="admin_contentmodules studip-grid" + tag="div" + role="listbox" + > + <div v-for="module in sortedModules" + :key="module.id" + role="option" + class="studip-grid-element" + :class="getModuleCSSClasses(module, activated[module.id])" + v-cloak + > + <div> + <a :class="'upper_part' + (module.active && filterCategory === null ? ' dragarea' : '')" :href="getDescriptionURL(module)" data-dialog> + <div> + <img :src="module.icon" width="40" height="40" v-if="module.icon"> + </div> + <div> + <h3>{{module.displayname}}</h3> + {{module.summary}} + </div> + </a> + <div class="down_part"> + <div> + <a class="dragarea" + tabindex="0" + :title="$gettextInterpolate('Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {module: module.displayname})" + @keydown="keyboardHandler($event, module)" + v-if="module.active && filterCategory === null" + :ref="`draghandle-${module.id}`"> + <span class="drag-handle"></span> + </a> + <label v-if="!module.mandatory"> + <input type="checkbox" :checked="activated[module.id]" @click="toggleModule(module)" :ref="'checkbox_' + module.id"> + {{ module.active ? $gettext('Werkzeug ist aktiv') : $gettext('Werkzeug ist inaktiv') }} + </label> + </div> + + <div class="icons_right"> + <a href="#" + class="toggle_visibility" + role="checkbox" + v-if="module.active && !module.mandatory" + :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'" + @click.prevent="toggleModuleVisibility(module)"> + <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'" + class="text-bottom" + :title="$gettextInterpolate($gettext('Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname})"></studip-icon> + </a> + <a :href="getRenameURL(module)" data-dialog="size=medium" v-if="module.active"> + <studip-icon shape="edit" + class="text-bottom" + :title="$gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), { name: module.displayname })"></studip-icon> + </a> + </div> + </div> + </div> + </div> + </transition-group> + </draggable> +</template> +<script> +import Vue from 'vue'; +import { mapState } from 'vuex'; +import ContentModulesMixin from '../mixins/ContentModulesMixin.js'; + +export default { + name: 'ContentModules', + mixins: [ContentModulesMixin], + data: () => ({ + activated: {}, + timeouts: {}, + }), + computed: { + ...mapState('contentmodules', [ + 'modules' + ]), + }, + methods: { + toggleModule(module) { + Vue.set(this.activated, module.id, !this.activated[module.id]); + + if (this.timeouts[module.id] ?? null) { + clearTimeout(this.timeouts[module.id] ?? null); + this.timeouts[module.id] = null; + } else { + this.timeouts[module.id] = setTimeout(() => { + this.toggleModuleActivation(module); + this.timeouts[module.id] = null; + }, 700); + } + }, + }, + watch: { + modules: { + immediate: true, + handler(current) { + current.forEach(module => Vue.set(this.activated, module.id, module.active)); + } + } + } +} +</script> +<style lang="scss" scoped> +.studip-grid-element { + display: flex; + flex-direction: row; + background-color: var(--white); + border-left: 1px solid var(--dark-gray-color-60); + transition: all 500ms ease, border-left-color 300ms ease; + &.visibility-visible { + border-left-color: var(--green); + > div { + border-left-color: var(--green); + } + } + &.visibility-invisible { + border-left-color: var(--yellow); + > div { + border-left-color: var(--yellow); + } + } + > div { + display: flex; + flex-direction: column; + justify-content: space-between; + transition: all 500ms ease, border-left-color 300ms ease; + border-left: 10px solid var(--dark-gray-color-60); + min-height: 150px; + width: 100%; + + > .upper_part { + display: flex; + > :first-child { + padding: 10px 5px 10px 15px; + } + > :last-child { + padding: 10px 10px 20px; + + h3 { + margin-top: 0; + color: var(--base-color); + } + } + } + > .down_part { + background-color: var(--content-color-20); + display: flex; + justify-content: space-between; + align-items: center; + min-height: 30px; + padding-left: 5px; + > div { + display: flex; + align-items: center; + } + .icons_right > a { + margin-right: 8px; + } + } + } +} +</style> diff --git a/resources/vue/components/ContentmodulesEditTable.vue b/resources/vue/components/ContentmodulesEditTable.vue new file mode 100644 index 0000000000000000000000000000000000000000..144f920a7ac1aa6b20037dd8660fbcc017433d14 --- /dev/null +++ b/resources/vue/components/ContentmodulesEditTable.vue @@ -0,0 +1,100 @@ +<template> + <table class="admin_contentmodules table default"> + <colgroup> + <col style="width: 20px" v-if="filterCategory === null"> + <col style="width: 20px"> + <col> + <col style="width: 24px"> + </colgroup> + <thead> + <tr> + <th v-if="filterCategory === null"></th> + <th></th> + <th>{{ $gettext('Name') }}</th> + <th class="actions">{{ $gettext('Aktionen') }}</th> + </tr> + </thead> + + <draggable v-model="sortedModules" handle=".dragarea" tag="tbody"> + <tr v-for="module in sortedModules" + :key="module.id" + :class="getModuleCSSClasses(module)" + v-cloak> + <td v-if="filterCategory === null"> + <a class="dragarea" + tabindex="0" + :title="$gettextInterpolate('Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {module: module.displayname})" + @keydown="keyboardHandler($event, module)" + v-if="module.active" + :ref="`draghandle-${module.id}`" + > + <span class="drag-handle"></span> + </a> + </td> + <td> + <input type="checkbox" + v-model="module.active" + @click="toggleModuleActivation(module)" + v-if="!module.mandatory" + :ref="'checkbox_' + module.id"> + </td> + <td> + <a class="upper_part" + :class="{ dragrea: module.active }" + :href="getDescriptionURL(module)" + data-dialog + > + <img :src="module.icon" width="20" height="20" v-if="module.icon" class="text-bottom"> + {{ module.displayname }} + </a> + </td> + <td class="actions"> + <a href="#" + v-if="module.active && !module.mandatory" + role="checkbox" + :aria-checked="module.visibility !== 'tutor' ? 'true' : 'false'" + @click.prevent="toggleModuleVisibility(module)"> + <studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'" + class="text-bottom" + :title="$gettextInterpolate($gettext('Inhaltsmoduls %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname })"></studip-icon> + </a> + <a :href="getRenameURL(module)" data-dialog="size=auto" v-if="module.active"> + <studip-icon shape="edit" class="text-bottom" :title="$gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), { name: module.displayname })"></studip-icon> + </a> + </td> + </tr> + </draggable> + </table> +</template> + +<script> +import ContentModulesMixin from '../mixins/ContentModulesMixin.js'; + +export default { + name: 'contentmodules-edit-table', + + mixins: [ContentModulesMixin], +} +</script> +<style lang="scss"> +@use '../../assets/stylesheets/mixins/colors.scss'; + +table.admin_contentmodules > tbody > tr { + > td:first-child { + background-image: linear-gradient(colors.$dark-gray-color-60, colors.$dark-gray-color-60); + background-repeat: no-repeat; + background-position: left; + background-size: 10px auto; + padding-left: 15px; + } + &.visibility-visible > td:first-child { + background-image: linear-gradient(colors.$green, colors.$green); + } + &.visibility-invisible > td:first-child { + background-image: linear-gradient(colors.$yellow, colors.$yellow); + } + > td { + height: 31px; //to make all rows equally high + } +} +</style> diff --git a/resources/vue/components/Datetimepicker.vue b/resources/vue/components/Datetimepicker.vue index 87f6dfd294087c829ffd45b269ce0dba7259df85..6b12ad78abc2896cf607f85601f1033058ddc4bd 100644 --- a/resources/vue/components/Datetimepicker.vue +++ b/resources/vue/components/Datetimepicker.vue @@ -33,8 +33,12 @@ export default { setUnixTimestamp () { let formatted_date = this.$refs.visibleInput.value; let date = formatted_date.match(/(\d+)/g); - date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`); - this.$emit('input', Math.floor(date / 1000)); + if (date) { + date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`); + this.$emit('input', Math.floor(date / 1000)); + } else { + this.$emit('input', null); + } } }, mounted () { diff --git a/resources/vue/components/I18nTextarea.vue b/resources/vue/components/I18nTextarea.vue index 79a810dbf29250025d0e7d9ede275d9a8a38e1c9..6d31d7b94643eedf516e82ba55a8b57bd2ff137a 100644 --- a/resources/vue/components/I18nTextarea.vue +++ b/resources/vue/components/I18nTextarea.vue @@ -144,12 +144,14 @@ export default { values[this.selectedLanguage.id] = this.value; this.values = values; } + this.$emit('selectlanguage', this.selectedLanguage.id); }, methods: { selectLanguage (e) { for (let i in this.languages) { if (e.target.value === this.languages[i].id) { this.selectedLanguage = this.languages[i]; + this.$emit('selectlanguage', this.languages[i].id); this.$nextTick(() => { if (typeof this.$refs.inputfield.focus === "function") { this.$refs.inputfield.focus(); diff --git a/resources/vue/mixins/ContentModulesMixin.js b/resources/vue/mixins/ContentModulesMixin.js new file mode 100644 index 0000000000000000000000000000000000000000..7df586dda28c6592d1a3246fca3f86babb34446f --- /dev/null +++ b/resources/vue/mixins/ContentModulesMixin.js @@ -0,0 +1,125 @@ +import draggable from 'vuedraggable'; +import { mapActions, mapState } from 'vuex'; + +export default { + components: { + draggable, + }, + data: () => ({ + order: [], + }), + computed: { + ...mapState('contentmodules', [ + 'categories', + 'filterCategory', + 'highlighted', + 'modules', + 'view', + ]), + activeModules() { + return this.sortedModules.filter(module => module.active); + }, + sortedModules: { + get() { + return Object.values(this.modules) + .filter(module => { + return this.filterCategory === null + || this.filterCategory === module.category; + }) + .sort(function (a, b) { + if (a.active && !b.active) { + return -1; + } else if (!a.active && b.active) { + return 1; + } else if (a.active) { + return a.position - b.position; + } else { + return a.displayname.localeCompare(b.displayname); + } + }); + }, + set(modules) { + let position = 0; + for (const key in modules) { + modules[key].position = position++; + } + this.exchangeModules(modules).then((output) => { + if (output.tabs) { + $('.tabs_wrapper').replaceWith(output.tabs); + } + }); + }, + }, + }, + methods: { + ...mapActions('contentmodules', [ + 'changeView', + 'exchangeModules', + 'setModuleActive', + 'setModuleVisible', + 'swapModules', + ]), + keyboardHandler(event, module) { + const activeIndex = this.activeModules.findIndex(m => m.id === module.id); + + let otherModule = null; + if (event.key === 'ArrowUp' && activeIndex > 0) { + otherModule = this.activeModules[activeIndex - 1]; + } else if (event.key === 'ArrowDown' && activeIndex !== this.activeModules.length - 1) { + otherModule = this.activeModules[activeIndex + 1]; + } + + if (otherModule === null) { + return; + } + + event.preventDefault(); + + this.swapModules({ + moduleA: module, + moduleB: otherModule, + }).then((output) => { + if (output.tabs) { + $('.tabs_wrapper').replaceWith(output.tabs); + } + }).then(() => { + this.$nextTick(() => { + this.$refs[`draghandle-${module.id}`][0].focus(); + }); + }); + }, + toggleModuleActivation(module) { + this.setModuleActive({ + moduleId: module.id, + active: !module.active, + }).then((output) => { + if (output.tabs) { + $('.tabs_wrapper').replaceWith(output.tabs); + } + }); + }, + toggleModuleVisibility(module) { + this.setModuleVisible({ + moduleId: module.id, + visible: module.visibility === 'tutor', + }).then((output) => { + if (output.tabs) { + $('.tabs_wrapper').replaceWith(output.tabs); + } + }); + }, + getRenameURL(module) { + return STUDIP.URLHelper.getURL(`dispatch.php/course/contentmodules/rename/${module.id}`); + }, + getDescriptionURL(module) { + return STUDIP.URLHelper.getURL(`dispatch.php/course/contentmodules/info/${module.id}`); + }, + getModuleCSSClasses(module, active= null) { + if (!(active ?? module.active)) { + return 'inactive'; + } + + return module.visibility === 'tutor' ? 'visibility-invisible' : 'visibility-visible'; + }, + }, +}; diff --git a/resources/vue/store/ContentModulesStore.js b/resources/vue/store/ContentModulesStore.js new file mode 100644 index 0000000000000000000000000000000000000000..9dc4609630f668a433eccaca598829529a84bc4a --- /dev/null +++ b/resources/vue/store/ContentModulesStore.js @@ -0,0 +1,124 @@ +export default { + namespaced: true, + + state: () => ({ + categories: [], + filterCategory: null, + highlighted: [], + modules: [], + userId: null, + view: 'tiles', + }), + getters: { + getModuleById: (state) => (moduleId) => { + return state.modules.find(module => module.id === moduleId); + }, + }, + mutations: { + setCategories(state, categories) { + state.categories = categories; + }, + setFilterCategory(state, category) { + state.filterCategory = category; + }, + setHighlighted(state, highlighted) { + state.highlighted = highlighted; + }, + setModule(state, module) { + let modules = state.modules.filter(m => m.id !== module.id); + modules.push(module); + + state.modules = modules; + }, + setModules(state, modules) { + state.modules = modules; + }, + setUserId(state, userId) { + state.userId = userId; + }, + setView(state, view) { + state.view = view; + }, + }, + actions: { + changeView({ commit, state }, view) { + commit('setView', view); + + const documentId = `${state.userId}_CONTENTMODULES_TILED_DISPLAY`; + + const data = { + id: documentId, + type: 'config-values', + attributes: { value: view === 'tiles' } + }; + + return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } }) ; + }, + exchangeModules({ commit, state }, modules) { + const order = modules.filter(module => module.active) + .sort((a, b) => a.position - b.position) + .map(module => module.id); + return $.post( + STUDIP.URLHelper.getURL('dispatch.php/course/contentmodules/reorder'), + { order } + ).then((output) => { + commit('setModules', modules); + + return output; + }); + }, + setModuleActive({ commit, state, getters }, { moduleId, active }) { + const module = getters.getModuleById(moduleId); + module.active = active; + + return $.post( + STUDIP.URLHelper.getURL('dispatch.php/course/contentmodules/trigger'), + { + moduleclass: module.moduleclass, + plugin_id: module.id, + active: module.active ? 1 : 0 + } + ).done((output) => { + module.position = output.position; + commit('setModule', module); + + return output; + }); + }, + setModuleVisible({ commit, state, getters }, { moduleId, visible }) { + const module = getters.getModuleById(moduleId); + + return $.post( + STUDIP.URLHelper.getURL('dispatch.php/course/contentmodules/change_visibility'), + { + moduleclass: module.moduleclass, + plugin_id: module.id, + visible: visible ? 1 : 0, + } + ).done((output) => { + module.visibility = output.visibility; + commit('setModule', module); + }); + }, + swapModules({ dispatch, state, getters }, { moduleA, moduleB }) { + let modules = state.modules.map(module => { + if (module.id === moduleA.id) { + return { + ...moduleA, + position: moduleB.position, + }; + } + + if (module.id === moduleB.id) { + return { + ...moduleB, + position: moduleA.position, + }; + } + + return module; + }); + return dispatch('exchangeModules', modules); + }, + } +} diff --git a/templates/forms/form.php b/templates/forms/form.php index 21eb9c80f83051e2255e342288690f5c3066dbe5..094805e9e597dcf9c646486fd970a825977d7f1f 100644 --- a/templates/forms/form.php +++ b/templates/forms/form.php @@ -24,6 +24,7 @@ $form_id = md5(uniqid()); novalidate id="<?= htmlReady($form_id) ?>" data-inputs="<?= htmlReady(json_encode($inputs)) ?>" + data-debugmode="<?= htmlReady(json_encode($form->getDebugMode())) ?>" data-required="<?= htmlReady(json_encode($required_inputs)) ?>" class="default studipform<?= $form->isCollapsable() ? ' collapsable' : '' ?>"> @@ -31,7 +32,7 @@ $form_id = md5(uniqid()); <article aria-live="assertive" class="validation_notes studip" - v-if="(STUDIPFORM_REQUIRED.length > 0) || STUDIPFORM_DISPLAYVALIDATION"> + v-if="STUDIPFORM_REQUIRED.length > 0 || STUDIPFORM_VALIDATIONNOTES.length > 0"> <header> <h1> <?= Icon::create('info-circle', Icon::ROLE_INFO)->asImg(17, ['class' => "text-bottom validation_notes_icon"]) ?> diff --git a/templates/forms/i18n_formatted_input.php b/templates/forms/i18n_formatted_input.php index 4e667f5f2bf8f518157827ad867feddfae790ffd..6466731b1a717605510b73e15a872f36b915a820 100644 --- a/templates/forms/i18n_formatted_input.php +++ b/templates/forms/i18n_formatted_input.php @@ -12,6 +12,7 @@ name="<?= htmlReady($name) ?>" value="<?= htmlReady($value) ?>" @allinputs="setInputs" + @selectlanguage="(language_id) => selectLanguage('<?= htmlReady($this->name) ?>', language_id)" :wysiwyg_disabled="<?= \Config::get()->WYSIWYG ? 'false' : 'true' ?>" <?= $required ? 'required' : '' ?>> </i18n-textarea> </div> diff --git a/templates/forms/i18n_textarea_input.php b/templates/forms/i18n_textarea_input.php index 3209b6827e5e723e43d5e22ebeadcdb17dbb8f88..d9b2ff3f809adf43d42954c527656d4ef4ff1fff 100644 --- a/templates/forms/i18n_textarea_input.php +++ b/templates/forms/i18n_textarea_input.php @@ -12,6 +12,7 @@ name="<?= htmlReady($this->name) ?>" value="<?= htmlReady($value) ?>" <?= $required ? 'required' : '' ?> + @selectlanguage="(language_id) => selectLanguage('<?= htmlReady($this->name) ?>', language_id)" @allinputs="setInputs"> </i18n-textarea> </div> diff --git a/templates/forms/info_input.php b/templates/forms/info_input.php new file mode 100644 index 0000000000000000000000000000000000000000..7461b7c20d5583eca0633dfddc1691ecf42b9db7 --- /dev/null +++ b/templates/forms/info_input.php @@ -0,0 +1,8 @@ +<div class="formpart"> + <article class="studip"> + <header> + <h1><?= htmlReady($title) ?></h1> + </header> + <?= formatReady($value) ?> + </article> +</div> diff --git a/templates/forms/wysiwyg_input.php b/templates/forms/wysiwyg_input.php new file mode 100644 index 0000000000000000000000000000000000000000..989bb5c731493d580c58435af0d7681b84ffa3b4 --- /dev/null +++ b/templates/forms/wysiwyg_input.php @@ -0,0 +1,16 @@ +<div class="formpart"> + <label<?= ($this->required ? ' class="studiprequired"' : '') ?> for="<?= $id ?>"> + <span class="textlabel"> + <?= htmlReady($this->title) ?> + </span> + <? if ($this->required) : ?> + <span class="asterisk" title="<?= _('Dies ist ein Pflichtfeld') ?>" aria-hidden="true">*</span> + <? endif ?> + </label> + <studip-wysiwyg + id="<?= $id ?>" + v-model="<?= htmlReady($name) ?>" + value="<?= htmlReady($value) ?>" + <?= $required ? 'required' : '' ?>> + </studip-wysiwyg> + </div>