aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan-Hendrik Willms <tleilax+studip@gmail.com>2024-07-04 13:31:50 +0000
committerJan-Hendrik Willms <tleilax+studip@gmail.com>2024-07-04 13:31:50 +0000
commita9c40e363b3e13223bb771105c190be9475021a5 (patch)
treee2bb7cd2aea0ac45cc2b17076b88f8ddca519f78
parentda240ca9af9136e52b125ed16ac9a31a396321fa (diff)
introduce [data-vue-app], fixes #4294
Closes #4294, #4295, #4297, #4298, #4301, #4304, and #4306 Merge request studip/studip!3109
-rw-r--r--app/controllers/admin/cache.php28
-rw-r--r--app/controllers/admin/courses.php8
-rw-r--r--app/controllers/admin/tree.php42
-rw-r--r--app/controllers/course/contentmodules.php22
-rw-r--r--app/controllers/my_courses.php20
-rw-r--r--app/controllers/search/courses.php15
-rw-r--r--app/views/admin/cache/settings.php14
-rw-r--r--app/views/admin/courses/index.php34
-rw-r--r--app/views/admin/tree/batch_assign_semtree.php10
-rw-r--r--app/views/admin/tree/rangetree.php9
-rw-r--r--app/views/admin/tree/semtree.php10
-rw-r--r--app/views/course/contentmodules/index.php1
-rw-r--r--app/views/course/contentmodules/info.php4
-rw-r--r--app/views/my_courses/index.php11
-rw-r--r--app/views/search/courses/index.php16
-rw-r--r--lib/classes/Debug/VueCollector.php97
-rw-r--r--lib/classes/StudipController.php11
-rw-r--r--lib/classes/VueApp.php194
-rw-r--r--resources/assets/javascripts/bootstrap/admin-courses.js26
-rw-r--r--resources/assets/javascripts/bootstrap/cache-admin.js21
-rw-r--r--resources/assets/javascripts/bootstrap/contentmodules.js45
-rw-r--r--resources/assets/javascripts/bootstrap/my-courses.js22
-rw-r--r--resources/assets/javascripts/bootstrap/responsive-navigation.js10
-rw-r--r--resources/assets/javascripts/bootstrap/treeview.js13
-rw-r--r--resources/assets/javascripts/bootstrap/vue.js80
-rw-r--r--resources/assets/javascripts/entry-base.js8
-rw-r--r--resources/assets/javascripts/lib/admin-courses.js15
-rw-r--r--resources/assets/javascripts/lib/studip-vue.js35
-rw-r--r--resources/vue/components/AdminCourses.vue254
-rw-r--r--resources/vue/components/CacheAdministration.vue11
-rw-r--r--templates/header.php12
-rw-r--r--templates/vue-app.php14
32 files changed, 690 insertions, 422 deletions
diff --git a/app/controllers/admin/cache.php b/app/controllers/admin/cache.php
index c016af5..7bd70c6 100644
--- a/app/controllers/admin/cache.php
+++ b/app/controllers/admin/cache.php
@@ -64,19 +64,21 @@ class Admin_CacheController extends AuthenticatedController
*/
public function settings_action()
{
- if ($this->enabled) {
- $this->types = CacheType::findAndMapBySQL(function (CacheType $type) {
- return $type->toArray();
- }, "1 ORDER BY `cache_id`");
-
- $currentCache = Config::get()->SYSTEMCACHE;
- $currentCacheClass = CacheType::findOneByClass_name($currentCache['type']);
- $this->cache = $currentCacheClass->class_name;
- $this->config = $currentCacheClass->class_name::getConfig();
- } else {
- PageLayout::postWarning(
- _('Caching ist systemweit ausgeschaltet, daher kann hier nichts konfiguriert werden.'));
- }
+ $currentCache = Config::get()->SYSTEMCACHE;
+ $currentCacheClass = CacheType::findOneByClass_name($currentCache['type']);
+
+ $this->render_vue_app(
+ Studip\VueApp::create('CacheAdministration')
+ ->withProps([
+ 'enabled' => (bool) $this->enabled,
+ 'currentCache' => $currentCacheClass->class_name,
+ 'currentConfig' => $currentCacheClass->class_name::getConfig(),
+ 'cacheTypes' => CacheType::findAndMapBySQL(
+ fn(CacheType $type) => $type->toArray(),
+ "1 ORDER BY `cache_id`"
+ ),
+ ])
+ );
}
/**
diff --git a/app/controllers/admin/courses.php b/app/controllers/admin/courses.php
index 5f60442..a10aa5c 100644
--- a/app/controllers/admin/courses.php
+++ b/app/controllers/admin/courses.php
@@ -305,15 +305,9 @@ class Admin_CoursesController extends AuthenticatedController
$this->fields = $this->getViewFilters();
$this->sortby = $GLOBALS['user']->cfg->MEINE_SEMINARE_SORT ?? (Config::get()->IMPORTANT_SEMNUMBER ? 'number' : 'name');
$this->sortflag = $GLOBALS['user']->cfg->MEINE_SEMINARE_SORT_FLAG ?? 'ASC';
+ $this->store_data = $this->getStoreData();
$this->buildSidebar();
-
- PageLayout::addHeadElement('script', [
- 'type' => 'text/javascript',
- ], sprintf(
- 'window.AdminCoursesStoreData = %s;',
- json_encode($this->getStoreData())
- ));
}
private function getStoreData(): array
diff --git a/app/controllers/admin/tree.php b/app/controllers/admin/tree.php
index c8f2a8f..ec35368 100644
--- a/app/controllers/admin/tree.php
+++ b/app/controllers/admin/tree.php
@@ -7,10 +7,28 @@ class Admin_TreeController extends AuthenticatedController
$GLOBALS['perm']->check('root');
Navigation::activateItem('/admin/locations/range_tree');
PageLayout::setTitle(_('Einrichtungshierarchie bearbeiten'));
- $this->startId = Request::get('node_id', 'RangeTreeNode_root');
+
$this->semester = Request::option('semester', Semester::findCurrent()->id);
$this->classname = RangeTreeNode::class;
$this->setupSidebar();
+
+ $this->render_vue_app(
+ Studip\VueApp::create('tree/StudipTree')
+ ->withProps([
+ 'breadcrumb-icon' => 'institute',
+ 'create-url' => $this->createURL(),
+ 'delete-url' => $this->deleteURL(),
+ 'edit-url' => $this->editURL(),
+ 'editable' => true,
+ 'semester' => $this->semester,
+ 'show-structure-as-navigation' => true,
+ 'start-id' => Request::get('node_id', 'RangeTreeNode_root'),
+ 'title' => _('Einrichtungshierarchie bearbeiten'),
+ 'view-type' => 'table',
+ 'visible-children-only' => false,
+ 'with-courses' => true,
+ ])
+ );
}
public function semtree_action()
@@ -18,10 +36,30 @@ class Admin_TreeController extends AuthenticatedController
$GLOBALS['perm']->check('root');
Navigation::activateItem('/admin/locations/sem_tree');
PageLayout::setTitle(_('Veranstaltungshierarchie bearbeiten'));
- $this->startId = Request::get('node_id', 'StudipStudyArea_root');
+
+
$this->semester = Request::option('semester', Semester::findCurrent()->id);
$this->classname = StudipStudyArea::class;
$this->setupSidebar();
+
+ $this->render_vue_app(
+ Studip\VueApp::create('tree/StudipTree')
+ ->withProps([
+ 'breadcrumb-icon' => 'literature',
+ 'create-url' => $this->createURL(),
+ 'delete-url' => $this->deleteURL(),
+ 'edit-url' => $this->editURL(),
+ 'editable' => true,
+ 'semester' => $this->semester,
+ 'show-structure-as-navigation' => true,
+ 'start-id' => Request::get('node_id', 'StudipStudyArea_root'),
+ 'title' => _('Veranstaltungshierarchie bearbeiten'),
+ 'view-type' => 'table',
+ 'visible-children-only' => false,
+ 'with-course-assign' => true,
+ 'with-courses' => true,
+ ])
+ );
}
/**
diff --git a/app/controllers/course/contentmodules.php b/app/controllers/course/contentmodules.php
index d37d1bb..d1ac7b4 100644
--- a/app/controllers/course/contentmodules.php
+++ b/app/controllers/course/contentmodules.php
@@ -83,18 +83,16 @@ class Course_ContentmodulesController extends AuthenticatedController
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',
- ])
- ));
+ $this->render_vue_app(
+ Studip\VueApp::create('ContentModules')
+ ->withStore('ContentModulesStore', 'contentmodules', [
+ '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()
diff --git a/app/controllers/my_courses.php b/app/controllers/my_courses.php
index 8205c12..e17fc91 100644
--- a/app/controllers/my_courses.php
+++ b/app/controllers/my_courses.php
@@ -113,13 +113,13 @@ class MyCoursesController extends AuthenticatedController
}
$this->setupSidebar($sem_key, $group_field, $this->check_for_new($sem_courses, $group_field));
- $data = $this->getMyCoursesData($sem_courses, $group_field);
- PageLayout::addHeadElement(
- 'script',
- ['type' => 'text/javascript'],
- 'window.STUDIP.MyCoursesData = ' . json_encode($data) . ';'
- );
+ $this->vueApp = Studip\VueApp::create('MyCourses')
+ ->withStore(
+ 'MyCoursesStore',
+ 'mycourses',
+ $this->getMyCoursesData($sem_courses, $group_field)
+ );
}
/**
@@ -797,10 +797,10 @@ class MyCoursesController extends AuthenticatedController
}
return [
- 'courses' => $this->sanitizeNavigations(array_map([$this, 'convertCourse'], $temp_courses)),
- 'groups' => $groups,
- 'user_id' => $GLOBALS['user']->id,
- 'config' => [
+ 'setCourses' => $this->sanitizeNavigations(array_map([$this, 'convertCourse'], $temp_courses)),
+ 'setGroups' => $groups,
+ 'setUserId' => $GLOBALS['user']->id,
+ 'setConfig' => [
'allow_dozent_visibility' => Config::get()->ALLOW_DOZENT_VISIBILITY,
'open_groups' => array_values($GLOBALS['user']->cfg->MY_COURSES_OPEN_GROUPS),
'sem_number' => Config::get()->IMPORTANT_SEMNUMBER,
diff --git a/app/controllers/search/courses.php b/app/controllers/search/courses.php
index a6a4d27..76e3320 100644
--- a/app/controllers/search/courses.php
+++ b/app/controllers/search/courses.php
@@ -59,6 +59,21 @@ class Search_CoursesController extends AuthenticatedController
$this->setupSidebar();
PageLayout::setTitle($title);
+
+ $this->render_vue_app(
+ Studip\VueApp::create('tree/StudipTree')
+ ->withProps([
+ 'breadcrumb-icon' => $this->breadcrumbIcon,
+ 'sem-class' => $this->semClass,
+ 'semester' => $this->semester,
+ 'start-id' => $this->startId,
+ 'title' => $this->treeTitle,
+ 'view-type' => $this->show_as,
+ 'with-courses' => true,
+ 'with-export' => true,
+ 'with-search' => true,
+ ])
+ );
}
private function setupSidebar()
diff --git a/app/views/admin/cache/settings.php b/app/views/admin/cache/settings.php
deleted file mode 100644
index cf022d4..0000000
--- a/app/views/admin/cache/settings.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-/**
- * @var boolean $enabled
- * @var array $types
- * @var array $config
- * @var string $cache
- */
-?>
-<? if ($enabled) : ?>
- <div id="cache-admin-container">
- <cache-administration :cache-types='<?= htmlReady(json_encode($types)) ?>' current-cache="<?= htmlReady($cache) ?>"
- :current-config='<?= htmlReady(json_encode($config)) ?>'></cache-administration>
- </div>
-<? endif;
diff --git a/app/views/admin/courses/index.php b/app/views/admin/courses/index.php
index 812790c..690366c 100644
--- a/app/views/admin/courses/index.php
+++ b/app/views/admin/courses/index.php
@@ -9,6 +9,7 @@
* @var string $sortflag
* @var array $activeSidebarElements
* @var int $max_show_courses
+ * @var array $store_data
*/
$unsortable_fields = [
@@ -17,29 +18,18 @@ $unsortable_fields = [
'contents'
];
?>
-
<? if (empty($insts)): ?>
<?= MessageBox::info(sprintf(_('Sie wurden noch keinen Einrichtungen zugeordnet. Bitte wenden Sie sich an einen der zuständigen %sAdministratoren%s.'), '<a href="' . URLHelper::getLink('dispatch.php/siteinfo/show') . '">', '</a>')) ?>
-<? else :
-
- $attributes = [
- ':show-complete' => json_encode((bool) Config::get()->ADMIN_COURSES_SHOW_COMPLETE),
- ':fields' => json_encode($fields),
- ':unsortable-fields' => json_encode($unsortable_fields),
- ':max-courses' => (int) $max_show_courses,
- 'sort-by' => $sortby,
- 'sort-flag' => $sortflag,
- ];
-?>
- <form method="post">
- <?= CSRFProtection::tokenTag() ?>
-
- <div class="admin-courses-vue-app course-admin"
- is="AdminCourses"
- v-cloak
- ref="app"
- <?= arrayToHtmlAttributes($attributes) ?>
- ></div>
- </form>
+<? else: ?>
+ <?= Studip\VueApp::create('AdminCourses')
+ ->withProps([
+ 'show-complete' => (bool) Config::get()->ADMIN_COURSES_SHOW_COMPLETE,
+ 'fields' => $fields,
+ 'unsortable-fields' => $unsortable_fields,
+ 'max-courses' => (int) $max_show_courses,
+ 'sort-by' => $sortby,
+ 'sort-flag' => $sortflag,
+ ])
+ ->withStore('AdminCoursesStore', 'admincourses', $store_data) ?>
<? endif; ?>
diff --git a/app/views/admin/tree/batch_assign_semtree.php b/app/views/admin/tree/batch_assign_semtree.php
index 4b1a4ff..6b7df0e 100644
--- a/app/views/admin/tree/batch_assign_semtree.php
+++ b/app/views/admin/tree/batch_assign_semtree.php
@@ -2,10 +2,12 @@
<?= CSRFProtection::tokenTag() ?>
<fieldset>
<legend><?= _('Studienbereichszuordnungen der ausgewählten Veranstaltungen bearbeiten') ?></legend>
- <div data-studip-tree>
- <studip-tree start-id="StudipStudyArea_root" :with-info="false" :open-levels="1"
- :assignable="true"></studip-tree>
- </div>
+ <?= Studip\VueApp::create('tree/StudipTree')->withProps([
+ 'assignable' => true,
+ 'open-levels' => 1,
+ 'start-id' => 'StudipStudyArea_root',
+ 'with-info' => false,
+ ]) ?>
</fieldset>
<fieldset>
<legend><?= _('Diese Veranstaltungen werden zugewiesen') ?></legend>
diff --git a/app/views/admin/tree/rangetree.php b/app/views/admin/tree/rangetree.php
deleted file mode 100644
index 1e3e945..0000000
--- a/app/views/admin/tree/rangetree.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<div data-studip-tree>
- <studip-tree start-id="<?= htmlReady($startId) ?>" view-type="table" breadcrumb-icon="institute"
- :with-search="false" :visible-children-only="false"
- :editable="true" edit-url="<?= $controller->url_for('admin/tree/edit') ?>"
- create-url="<?= $controller->url_for('admin/tree/create') ?>"
- delete-url="<?= $controller->url_for('admin/tree/delete') ?>"
- :with-courses="true" semester="<?= htmlReady($semester) ?>" :show-structure-as-navigation="true"
- title="<?= _('Einrichtungshierarchie bearbeiten') ?>"></studip-tree>
-</div>
diff --git a/app/views/admin/tree/semtree.php b/app/views/admin/tree/semtree.php
deleted file mode 100644
index 0c48245..0000000
--- a/app/views/admin/tree/semtree.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<div data-studip-tree>
- <studip-tree start-id="<?= htmlReady($startId) ?>" view-type="table" breadcrumb-icon="literature"
- :with-search="false" :visible-children-only="false"
- :editable="true" edit-url="<?= $controller->url_for('admin/tree/edit') ?>"
- create-url="<?= $controller->url_for('admin/tree/create') ?>"
- delete-url="<?= $controller->url_for('admin/tree/delete') ?>"
- :show-structure-as-navigation="true" :with-course-assign="true"
- :with-courses="true" semester="<?= htmlReady($semester) ?>"
- title="<?= _('Veranstaltungshierarchie bearbeiten') ?>"></studip-tree>
-</div>
diff --git a/app/views/course/contentmodules/index.php b/app/views/course/contentmodules/index.php
deleted file mode 100644
index af8f3e1..0000000
--- a/app/views/course/contentmodules/index.php
+++ /dev/null
@@ -1 +0,0 @@
-<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
index 93d3ad9..0585ebe 100644
--- a/app/views/course/contentmodules/info.php
+++ b/app/views/course/contentmodules/info.php
@@ -18,7 +18,9 @@
<? endif; ?>
</div>
</div>
- <div class="content-modules-controls-vue-app" is="ContentModulesControl" module_id="<?= htmlReady($plugin->getPluginId()) ?>"></div>
+ <?= Studip\VueApp::create('ContentModulesControl')->withProps([
+ 'module_id' => (string) $plugin->getPluginId(),
+ ]) ?>
<? $keywords = preg_split( "/;/", $metadata['keywords'] ?? '', -1, PREG_SPLIT_NO_EMPTY) ?>
<? if (count($keywords) > 0) : ?>
<ul class="keywords">
diff --git a/app/views/my_courses/index.php b/app/views/my_courses/index.php
index 1bbfd73..8ec7703 100644
--- a/app/views/my_courses/index.php
+++ b/app/views/my_courses/index.php
@@ -1,10 +1,15 @@
+<?php
+/**
+ * @var Studip\VueApp $vueApp
+ * @var array $my_bosses
+ * @var array $waiting_list
+ */
+?>
<? if ($waiting_list) : ?>
<?= $this->render_partial('my_courses/waiting_list.php', compact('waiting_list')) ?>
<? endif ?>
-<div class="my-courses-vue-app">
- <my-courses />
-</div>
+<?= $vueApp->render() ?>
<? if (count($my_bosses) > 0) : ?>
<?= $this->render_partial('my_courses/_deputy_bosses'); ?>
diff --git a/app/views/search/courses/index.php b/app/views/search/courses/index.php
deleted file mode 100644
index 500c31e..0000000
--- a/app/views/search/courses/index.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-/**
- * @var String $startId
- * @var String $show_as
- * @var String $treeTitle
- * @var String $breadcrumIcon
- * @var String $semester
- * @var String $semClass
- */
-?>
-<div data-studip-tree>
- <studip-tree start-id="<?= htmlReady($startId) ?>" view-type="<?= htmlReady($show_as) ?>" :visible-children-only="true"
- title="<?= htmlReady($treeTitle) ?>" breadcrumb-icon="<?= htmlReady($breadcrumbIcon) ?>"
- :with-search="true" :with-export="true" :with-courses="true" semester="<?= htmlReady($semester) ?>"
- :sem-class="<?= htmlReady($semClass) ?>" :with-export="true"></studip-tree>
-</div>
diff --git a/lib/classes/Debug/VueCollector.php b/lib/classes/Debug/VueCollector.php
new file mode 100644
index 0000000..a2d90f3
--- /dev/null
+++ b/lib/classes/Debug/VueCollector.php
@@ -0,0 +1,97 @@
+<?php
+namespace Studip\Debug;
+
+use DebugBar\DataCollector\DataCollector;
+use DebugBar\DataCollector\Renderable;
+use Studip\VueApp;
+
+final class VueCollector extends DataCollector implements Renderable
+{
+ public function __construct(
+ private readonly VueApp $app
+ ) {
+ $this->useHtmlVarDumper(false);
+ }
+
+ public function collect()
+ {
+ $data = [];
+
+ $props = $this->app->getProps();
+ if (count($props) > 0) {
+ ksort($props);
+
+ $data['== DATA =='] = count($props) . ' items';
+ foreach ($props as $key => $value) {
+ $data[$key] = $this->dumpVar($value);
+ }
+ }
+
+ $stores = $this->app->getStores();
+ if (count($stores) > 0) {
+ ksort($stores);
+ $storeData = $this->app->getStoreData();
+
+ $data['== STORES =='] = '';
+
+ foreach ($stores as $index => $store) {
+ $data[$index] = $store === $index ? '' : "({$store})";
+
+ $tmp = $storeData[$index] ?? [];
+ ksort($tmp);
+ foreach ($tmp as $key => $value) {
+ $data["- {$key}"] = $this->dumpVar($value);
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ public function getName()
+ {
+ return '[Vue]' . basename($this->app->getBaseComponent());
+ }
+
+ /**
+ * @return array
+ */
+ public function getAssets()
+ {
+ return $this->isHtmlVarDumperUsed() ? $this->getVarDumper()->getAssets() : [];
+ }
+
+ /**
+ * @return array[]
+ */
+ public function getWidgets()
+ {
+ $name = $this->getName();
+ $widget = $this->isHtmlVarDumperUsed()
+ ? 'PhpDebugBar.Widgets.HtmlVariableListWidget'
+ : 'PhpDebugBar.Widgets.VariableListWidget';
+
+
+ return [
+ $name => [
+ 'icon' => 'code',
+ 'widget' => $widget,
+ 'map' => $name,
+ 'default' => '{}'
+ ],
+ ];
+ }
+
+ private function dumpVar(mixed $variable): string
+ {
+ if ($this->isHtmlVarDumperUsed()) {
+ return $this->getVarDumper()->renderVar($variable);
+ }
+
+ if (!is_string($variable)) {
+ return $this->getDataFormatter()->formatVar($variable);
+ }
+
+ return $variable;
+ }
+}
diff --git a/lib/classes/StudipController.php b/lib/classes/StudipController.php
index a908a47..f99511c 100644
--- a/lib/classes/StudipController.php
+++ b/lib/classes/StudipController.php
@@ -589,6 +589,17 @@ abstract class StudipController extends Trails\Controller
}
/**
+ * Renders a vue app
+ *
+ * Use this if the vue app is the only content located on the page so
+ * you don't have to create a template file.
+ */
+ public function render_vue_app(\Studip\VueApp $app): void
+ {
+ $this->render_template($app->getTemplate(), $this->layout);
+ }
+
+ /**
* relays current request to another controller and returns the response
* the other controller is given all assigned properties, additional parameters are passed
* through
diff --git a/lib/classes/VueApp.php b/lib/classes/VueApp.php
new file mode 100644
index 0000000..0e20961
--- /dev/null
+++ b/lib/classes/VueApp.php
@@ -0,0 +1,194 @@
+<?php
+namespace Studip;
+
+use Flexi\Template;
+use Stringable;
+
+/**
+ * PHP abstraction of vue app
+ *
+ * The VueApp is used to create a Vue app in a general way. Just create it
+ * using the name of the case component and pass in any required props or
+ * stores including initial data.
+ *
+ * The store data is passed as an associative array where the key is the name
+ * of the mutation to call with the given value as data.
+ *
+ * All methods are written in fluid manner so that you can create the app like this:
+ *
+ * <code>
+ * <?= Studip\VueApp::create('ExampleComponent')
+ * ->withProps(['foo' => 'bar'])
+ * ->withStore('exampleStore', data: ['setBar' => 'baz']) ?>
+ * </code>
+ *
+ * All with* methods will always create a new cloned instance so the original
+ * instance is immutable.
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @since Stud.IP 6.0
+ */
+final class VueApp implements Stringable
+{
+ /**
+ * Creates a vue app with the given base component.
+ */
+ public static function create(string $base_component): VueApp
+ {
+ return new self($base_component);
+ }
+
+ private array $props = [];
+ private array $stores = [];
+ private array $storeData = [];
+
+ /**
+ * Private constructor since we want to enforce the use of VueApp::create().
+ */
+ private function __construct(
+ private readonly string $base_component
+ ) {
+ }
+
+ /**
+ * Returns the base component
+ */
+ public function getBaseComponent(): string
+ {
+ return $this->base_component;
+ }
+
+ /**
+ * Add props
+ *
+ * You may choose to overwrite the defined props
+ */
+ public function withProps(array $props, bool $overwrite = false): VueApp
+ {
+ $clone = clone $this;
+ $clone->props = [...$overwrite ? [] : $clone->props, ...$props];
+ return $clone;
+ }
+
+ /**
+ * Returns all props
+ */
+ public function getProps(): array
+ {
+ return $this->props;
+ }
+
+ /**
+ * Add a slot with the given name
+ *
+ * If you pass a flexi template as the content, it will be rendered.
+ */
+ public function withSlot(string $name, string|Template $content): VueApp
+ {
+ $this->slots[$name] = $content instanceof Template ? $content->render() : $content;
+ return $this;
+ }
+
+ /**
+ * Returns all slots
+ */
+ public function getSlots(): array
+ {
+ return $this->slots;
+ }
+
+ /**
+ * Adds a store
+ */
+ public function withStore(string $store, ?string $index = null, ?array $data = null): VueApp
+ {
+ $clone = clone $this;
+
+ $clone->stores[$index ?? $store] = $store;
+
+ if ($data !== null) {
+ $clone->storeData[$index ?? $store] = $data;
+ }
+
+ return $clone;
+ }
+
+ /**
+ * Returns all stores
+ */
+ public function getStores(): array
+ {
+ return $this->stores;
+ }
+
+ /**
+ * Returns all store data
+ */
+ public function getStoreData(): array
+ {
+ return $this->storeData;
+ }
+
+ /**
+ * Returns the template to render the vue app
+ */
+ public function getTemplate(): Template
+ {
+ $data = [
+ 'components' => [$this->base_component],
+ ];
+
+ if (count($this->stores) > 0) {
+ $data['stores'] = $this->stores;
+ }
+
+ $template = $GLOBALS['template_factory']->open('vue-app.php');
+ $template->baseComponent = basename($this->base_component);
+ $template->attributes = ['data-vue-app' => json_encode($data)];
+ $template->props = $this->getPreparedProps();
+ $template->storeData = $this->storeData;
+ return $template;
+ }
+
+ /**
+ * Returns the props as required to include them in the html
+ */
+ private function getPreparedProps(): array
+ {
+ $result = [];
+ foreach ($this->props as $name => $value) {
+ $name = ltrim($name, ':');
+ $name = strtokebabcase($name);
+ $result[":{$name}"] = json_encode($value);
+ }
+ return $result;
+ }
+
+ /**
+ * Renders the vue app
+ */
+ public function render(): string
+ {
+ if (Debug\DebugBar::isActivated()) {
+ $debugbar = app()->get(\DebugBar\DebugBar::class);
+ $collector = new Debug\VueCollector($this);
+ $debugbar->addCollector($collector);
+ }
+
+ \NotificationCenter::postNotification('VueAppWillRender', $this);
+
+ $content = $this->getTemplate()->render();
+
+ \NotificationCenter::postNotification('VueAppDidRender', $this);
+
+ return $content;
+ }
+
+ /**
+ * Returns a string representation of the vue app by rendering it.
+ */
+ public function __toString(): string
+ {
+ return $this->render();
+ }
+}
diff --git a/resources/assets/javascripts/bootstrap/admin-courses.js b/resources/assets/javascripts/bootstrap/admin-courses.js
index 74802b6..273b97d 100644
--- a/resources/assets/javascripts/bootstrap/admin-courses.js
+++ b/resources/assets/javascripts/bootstrap/admin-courses.js
@@ -1,30 +1,4 @@
STUDIP.domReady(() => {
- const node = document.querySelector('.admin-courses-vue-app');
- if (!node) {
- return;
- }
-
- Promise.all([
- STUDIP.Vue.load(),
- import('../../../vue/store/AdminCoursesStore.js').then((config) => config.default),
- import('../../../vue/components/AdminCourses.vue').then((component) => component.default),
- ]).then(([{ createApp, store }, storeConfig, AdminCourses]) => {
- store.registerModule('admincourses', storeConfig);
-
- Object.entries(window.AdminCoursesStoreData ?? {}).forEach(([key, value]) => {
- store.commit(`admincourses/${key}`, value);
- })
-
- const vm = createApp({
- components: { AdminCourses },
- });
- vm.$mount(node);
-
- STUDIP.AdminCourses.App = vm.$refs.app;
- });
-
-
-
$('.admin-courses-options').find('.options-radio, .options-checkbox').on('click', function () {
$(this).toggleClass(['options-checked', 'options-unchecked']);
$(this).attr('aria-checked', $(this).is('.options-checked') ? 'true' : 'false');
diff --git a/resources/assets/javascripts/bootstrap/cache-admin.js b/resources/assets/javascripts/bootstrap/cache-admin.js
deleted file mode 100644
index 97b4107..0000000
--- a/resources/assets/javascripts/bootstrap/cache-admin.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Stud.IP: Administration of available cache types, like database, Memcached, Redis etc.
- *
- * @author Thomas Hackl <studip@thomas-hackl.name>
- * @license GPL2 or any later version
- * @copyright Stud.IP core group
- * @since Stud.IP 5.0
- */
-
-import CacheAdministration from '../../../vue/components/CacheAdministration.vue'
-
-STUDIP.domReady(() => {
- if (document.getElementById('cache-admin-container')) {
- STUDIP.Vue.load().then(({ createApp }) => {
- createApp({
- el: '#cache-admin-container',
- components: { CacheAdministration }
- })
- })
- }
-});
diff --git a/resources/assets/javascripts/bootstrap/contentmodules.js b/resources/assets/javascripts/bootstrap/contentmodules.js
deleted file mode 100644
index 18963fd..0000000
--- a/resources/assets/javascripts/bootstrap/contentmodules.js
+++ /dev/null
@@ -1,45 +0,0 @@
-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((event) => {
- let target = event.target ?? document;
- if (target instanceof jQuery) {
- target = target.get(0);
- }
-
- const node = target.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/my-courses.js b/resources/assets/javascripts/bootstrap/my-courses.js
deleted file mode 100644
index 40e0c24..0000000
--- a/resources/assets/javascripts/bootstrap/my-courses.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import MyCourses from '../../../vue/components/MyCourses.vue';
-import storeConfig from '../../../vue/store/MyCoursesStore.js';
-
-STUDIP.domReady(async () => {
- if ($('.my-courses-vue-app').length === 0) {
- return;
- }
-
- const { createApp, store } = await STUDIP.Vue.load();
-
- store.registerModule('mycourses', storeConfig);
-
- store.commit('mycourses/setCourses', window.STUDIP.MyCoursesData['courses']);
- store.commit('mycourses/setGroups', window.STUDIP.MyCoursesData['groups']);
- store.commit('mycourses/setUserId', window.STUDIP.MyCoursesData['user_id']);
- store.commit('mycourses/setConfig', window.STUDIP.MyCoursesData['config']);
-
- const vm = createApp({
- components: { MyCourses }
- });
- vm.$mount('.my-courses-vue-app');
-});
diff --git a/resources/assets/javascripts/bootstrap/responsive-navigation.js b/resources/assets/javascripts/bootstrap/responsive-navigation.js
deleted file mode 100644
index ad39d2b..0000000
--- a/resources/assets/javascripts/bootstrap/responsive-navigation.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import ResponsiveNavigation from '../../../vue/components/responsive/ResponsiveNavigation.vue';
-
-STUDIP.domReady(() => {
- STUDIP.Vue.load().then(({ createApp }) => {
- createApp({
- el: '#responsive-menu',
- components: { ResponsiveNavigation }
- });
- });
-});
diff --git a/resources/assets/javascripts/bootstrap/treeview.js b/resources/assets/javascripts/bootstrap/treeview.js
deleted file mode 100644
index d132775..0000000
--- a/resources/assets/javascripts/bootstrap/treeview.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import StudipTree from '../../../vue/components/tree/StudipTree.vue'
-
-STUDIP.ready(() => {
- document.querySelectorAll('[data-studip-tree]:not(.vueified)').forEach(element => {
- element.classList.add('vueified');
- STUDIP.Vue.load().then(({ createApp }) => {
- createApp({
- el: element,
- components: { StudipTree }
- })
- })
- });
-});
diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js
index c6816a2..64d2492 100644
--- a/resources/assets/javascripts/bootstrap/vue.js
+++ b/resources/assets/javascripts/bootstrap/vue.js
@@ -1,46 +1,66 @@
-/**
- * The following block of code is used to automatically register your
- * Vue components. It will recursively scan this directory for the Vue
- * components and automatically register them with their "basename".
- *
- * Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
- */
STUDIP.ready(() => {
- $('[data-vue-app]').each(function () {
- if ($(this).is('[data-vue-app-created]')) {
- return;
- }
-
- const config = Object.assign({}, {
- id: false,
- components: [],
- store: false
- }, $(this).data().vueApp);
-
- let data = {};
- if (config.id && window.STUDIP.AppData && window.STUDIP.AppData[config.id] !== undefined) {
- data = window.STUDIP.AppData[config.id];
- }
+ document.querySelectorAll('[data-vue-app]:not([data-vue-app-created])').forEach((node) => {
+ const config = Object.assign(
+ {
+ components: [],
+ stores: {}
+ },
+ JSON.parse(node.dataset.vueApp)
+ );
let components = {};
config.components.forEach(component => {
- components[component] = () => import(`../../../vue/components/${component}.vue`);
+ const name = component.split('/').reverse()[0];
+ components[name] = () => import(`../../../vue/components/${component}.vue`);
});
STUDIP.Vue.load().then(async ({createApp, store}) => {
- if (config.store) {
- const storeConfig = await import(`../../../vue/store/${config.store}.js`);
+ for (const [index, name] of Object.entries(config.stores)) {
+ import(`../../../vue/store/${name}.js`).then(storeConfig => {
+ store.registerModule(index, storeConfig.default);
- store.registerModule(config.id, storeConfig.default, {root: true});
+ const dataElement = document.getElementById(`vue-store-data-${index}`);
+ if (dataElement) {
+ const data = JSON.parse(dataElement.innerText);
+ Object.keys(data).forEach(command => {
+ store.commit(`${index}/${command}`, data[command]);
+ });
- Object.keys(data).forEach(command => {
- store.commit(`${config.id}/${command}`, data[command]);
+ dataElement.remove();
+ }
});
}
+ createApp({
+ components,
+ store,
- createApp({components, data}).$mount(this);
+ beforeCreate() {
+ STUDIP.Vue.emit('VueAppWillCreate', this);
+ },
+ created() {
+ STUDIP.Vue.emit('VueAppDidCreate', this);
+ },
+ beforeMount() {
+ STUDIP.Vue.emit('VueAppWillMount', this);
+ },
+ mounted() {
+ STUDIP.Vue.emit('VueAppDidMount', this);
+ },
+ beforeUpdate() {
+ STUDIP.Vue.emit('VueAppWillUpdate', this);
+ },
+ updated() {
+ STUDIP.Vue.emit('VueAppDidUpdate', this);
+ },
+ beforeDestroy() {
+ STUDIP.Vue.emit('VueAppWillDestroy', this);
+ },
+ destroyed() {
+ STUDIP.Vue.emit('VueAppDidDestroy', this);
+ },
+ }).$mount(node);
});
- $(this).attr('data-vue-app-created', '');
+ node.dataset.vueAppCreated = 'true';
});
});
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js
index f551c6d..3c2223e 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-base.js
@@ -16,10 +16,6 @@ import "./init.js"
import "./bootstrap/responsive.js"
import "./bootstrap/vue.js"
-import "./bootstrap/system-notifications.js"
-
-import "./bootstrap/my-courses.js";
-
import "./studip-ui.js"
import "./bootstrap/fullscreen.js"
import "./bootstrap/tfa.js"
@@ -79,12 +75,8 @@ import "./bootstrap/blubber.js"
import "./bootstrap/consultations.js"
import "./bootstrap/scroll_to_top.js"
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"
import "./bootstrap/external_pages.js"
diff --git a/resources/assets/javascripts/lib/admin-courses.js b/resources/assets/javascripts/lib/admin-courses.js
index 23cf6dd..a7bd389 100644
--- a/resources/assets/javascripts/lib/admin-courses.js
+++ b/resources/assets/javascripts/lib/admin-courses.js
@@ -1,7 +1,18 @@
const AdminCourses = {
- App: null,
+ App: {
+ loadCourse(courseId) {
+ STUDIP.Vue.emit('AdminCourses/loadCourse', courseId);
+ },
+ changeFilter(filters) {
+ STUDIP.Vue.emit('AdminCourses/changeFilter', filters);
+ },
+ changeActionArea(area) {
+ STUDIP.Vue.emit('AdminCourses/changeActionArea', area);
+ }
+ },
changeFiltersDependendOnInstitute(institut_id) {
- STUDIP.AdminCourses.App.changeFilter({ institut_id });
+ AdminCourses.App.changeFilter({ institut_id });
+
//change Studiengangteil filter
$.get(
STUDIP.URLHelper.getURL('dispatch.php/admin/courses/get_stdgangteil_selector/' + institut_id)
diff --git a/resources/assets/javascripts/lib/studip-vue.js b/resources/assets/javascripts/lib/studip-vue.js
index c7cf89a..6ebd59f 100644
--- a/resources/assets/javascripts/lib/studip-vue.js
+++ b/resources/assets/javascripts/lib/studip-vue.js
@@ -1,15 +1,26 @@
-const load = async function () {
- return STUDIP.loadChunk('vue');
-};
+class Vue
+{
+ static async load()
+ {
+ return STUDIP.loadChunk('vue');
+ }
-const on = async function (...args) {
- const { eventBus } = await load();
- eventBus.on(...args);
-};
+ static async on(...args)
+ {
+ const { eventBus } = await this.load();
+ eventBus.on(...args);
+ }
-const emit = async function (...args) {
- const { eventBus } = await load();
- eventBus.emit(...args);
-};
+ static async off(...args) {
+ const { eventBus } = await this.load();
+ eventBus.off(...args);
+}
-export default { load, on, emit };
+ static async emit(...args)
+ {
+ const { eventBus } = await this.load();
+ eventBus.emit(...args);
+ }
+}
+
+export default Vue;
diff --git a/resources/vue/components/AdminCourses.vue b/resources/vue/components/AdminCourses.vue
index d24a900..2571396 100644
--- a/resources/vue/components/AdminCourses.vue
+++ b/resources/vue/components/AdminCourses.vue
@@ -1,106 +1,109 @@
<template>
- <table class="default">
- <caption>
- {{ $gettext('Veranstaltungen') }}
- <span class="actions" v-if="isLoading">
- <img :src="loadingIndicator" width="20" height="20" :title="$gettext('Daten werden geladen')">
- </span>
- <span class="actions" v-else-if="coursesCount > 0">
- {{ coursesCount + ' ' + $gettext('Veranstaltungen') }}
- </span>
- </caption>
- <thead>
- <tr class="sortable">
- <th v-if="showComplete" :class="sort.by === 'completion' ? 'sort' + sort.direction.toLowerCase() : ''">
- <a
- @click.prevent="changeSort('completion')"
- class="course-completion"
- :title="$gettext('Bearbeitungsstatus')"
- >
- {{ $gettext('Bearbeitungsstatus') }}
- </a>
- </th>
- <th v-for="activeField in sortedActivatedFields" :key="`field-${activeField}`" :class="sort.by === activeField ? 'sort' + sort.direction.toLowerCase() : ''">
- <a href="#"
- @click.prevent="changeSort(activeField)"
- :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}, true) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}, true) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}, true))"
- v-if="!unsortableFields.includes(activeField)"
- >
- {{ fields[activeField] }}
- </a>
- <template v-else>
- {{ fields[activeField] }}
- </template>
- </th>
- <th class="actions">
- {{ $gettext('Aktion') }}
- <studip-action-menu class="filter" :title="$gettext('Darstellungsfilter')" :items="availableFields" @toggleActiveField="toggleActiveField"></studip-action-menu>
- </th>
- </tr>
- <tr v-if="buttons.top">
- <th v-html="buttons.top" style="text-align: right" :colspan="colspan"></th>
- </tr>
- </thead>
- <tbody :class="{ loading: isLoading }">
- <tr v-for="course in sortedCourses"
- :key="course.id"
- :class="course.id === currentLine ? 'selected' : ''"
- @click="currentLine = course.id">
- <td v-if="showComplete">
- <button :href="getURL('dispatch.php/admin/courses/toggle_complete/' + course.id)"
- class="course-completion undecorated"
- :data-course-completion="course.completion"
- :title="(course.completion > 0 ? (course.completion == 1 ? $gettext('Veranstaltung in Bearbeitung.') : $gettext('Veranstaltung komplett.')) : $gettext('Veranstaltung neu.')) + ' ' + $gettext('Klicken zum Ändern des Status.')"
- @click.prevent="toggleCompletionState(course.id)">
- {{ $gettext('Bearbeitungsstatus ändern') }}
- </button>
- </td>
- <td v-for="active_field in sortedActivatedFields" :key="active_field">
- <div v-html="course[active_field]"></div>
- <a v-if="active_field === 'name' && getChildren(course).length > 0"
- @click.prevent="toggleOpenChildren(course.id)"
- href="">
- <studip-icon :shape="open_children.indexOf(course.id) === -1 ? 'add' : 'remove'" class="text-bottom"></studip-icon>
- {{ $gettextInterpolate(
- $gettext('%{ n } Unterveranstaltungen'),
- { n: getChildren(course).length }
- ) }}
- </a>
- </td>
- <td class="actions" v-html="course.action">
- </td>
- </tr>
- <tr v-if="coursesCount === 0 && coursesLoaded">
- <td :colspan="colspan">
- {{ $gettext('Keine Ergebnisse') }}
- </td>
- </tr>
- <tr v-if="coursesCount > 0 && sortedCourses.length === 0">
- <td :colspan="colspan">
- {{
- $gettextInterpolate(
- $gettext(`%{ n } Veranstaltungen entsprechen Ihrem Filter. Schränken Sie nach Möglichkeit die Filter weiter ein.`),
- { n: coursesCount }
- )
- }}
- <a href="" @click.prevent="loadCourses({withoutLimit: true});">
- {{ $gettext('Alle anzeigen') }}
- </a>
- </td>
- </tr>
- <tr v-if="!coursesLoaded">
- <td :colspan="colspan">
- {{ $gettext('Daten werden geladen ...') }}
- </td>
- </tr>
- </tbody>
- <tfoot v-if="buttons.bottom">
- <tr>
- <td v-html="buttons.bottom" style="text-align: right" :colspan="colspan"></td>
- </tr>
- </tfoot>
- </table>
+ <form method="post">
+ <input type="hidden" :name="csrf.name" :value="csrf.value">
+ <table class="default course-admin">
+ <caption>
+ {{ $gettext('Veranstaltungen') }}
+ <span class="actions" v-if="isLoading">
+ <img :src="loadingIndicator" width="20" height="20" :title="$gettext('Daten werden geladen')">
+ </span>
+ <span class="actions" v-else-if="coursesCount > 0">
+ {{ coursesCount + ' ' + $gettext('Veranstaltungen') }}
+ </span>
+ </caption>
+ <thead>
+ <tr class="sortable">
+ <th v-if="showComplete" :class="sort.by === 'completion' ? 'sort' + sort.direction.toLowerCase() : ''">
+ <a
+ @click.prevent="changeSort('completion')"
+ class="course-completion"
+ :title="$gettext('Bearbeitungsstatus')"
+ >
+ {{ $gettext('Bearbeitungsstatus') }}
+ </a>
+ </th>
+ <th v-for="activeField in sortedActivatedFields" :key="`field-${activeField}`" :class="sort.by === activeField ? 'sort' + sort.direction.toLowerCase() : ''">
+ <a href="#"
+ @click.prevent="changeSort(activeField)"
+ :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}, true) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}, true) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}, true))"
+ v-if="!unsortableFields.includes(activeField)"
+ >
+ {{ fields[activeField] }}
+ </a>
+ <template v-else>
+ {{ fields[activeField] }}
+ </template>
+ </th>
+ <th class="actions">
+ {{ $gettext('Aktion') }}
+ <studip-action-menu class="filter" :title="$gettext('Darstellungsfilter')" :items="availableFields" @toggleActiveField="toggleActiveField"></studip-action-menu>
+ </th>
+ </tr>
+ <tr v-if="buttons.top">
+ <th v-html="buttons.top" style="text-align: right" :colspan="colspan"></th>
+ </tr>
+ </thead>
+ <tbody :class="{ loading: isLoading }">
+ <tr v-for="course in sortedCourses"
+ :key="course.id"
+ :class="course.id === currentLine ? 'selected' : ''"
+ @click="currentLine = course.id">
+ <td v-if="showComplete">
+ <button :href="getURL('dispatch.php/admin/courses/toggle_complete/' + course.id)"
+ class="course-completion undecorated"
+ :data-course-completion="course.completion"
+ :title="(course.completion > 0 ? (course.completion == 1 ? $gettext('Veranstaltung in Bearbeitung.') : $gettext('Veranstaltung komplett.')) : $gettext('Veranstaltung neu.')) + ' ' + $gettext('Klicken zum Ändern des Status.')"
+ @click.prevent="toggleCompletionState(course.id)">
+ {{ $gettext('Bearbeitungsstatus ändern') }}
+ </button>
+ </td>
+ <td v-for="active_field in sortedActivatedFields" :key="active_field">
+ <div v-html="course[active_field]"></div>
+ <a v-if="active_field === 'name' && getChildren(course).length > 0"
+ @click.prevent="toggleOpenChildren(course.id)"
+ href="">
+ <studip-icon :shape="open_children.indexOf(course.id) === -1 ? 'add' : 'remove'" class="text-bottom"></studip-icon>
+ {{ $gettextInterpolate(
+ $gettext('%{ n } Unterveranstaltungen'),
+ { n: getChildren(course).length }
+ ) }}
+ </a>
+ </td>
+ <td class="actions" v-html="course.action">
+ </td>
+ </tr>
+ <tr v-if="coursesCount === 0 && coursesLoaded">
+ <td :colspan="colspan">
+ {{ $gettext('Keine Ergebnisse') }}
+ </td>
+ </tr>
+ <tr v-if="coursesCount > 0 && sortedCourses.length === 0">
+ <td :colspan="colspan">
+ {{
+ $gettextInterpolate(
+ $gettext(`%{ n } Veranstaltungen entsprechen Ihrem Filter. Schränken Sie nach Möglichkeit die Filter weiter ein.`),
+ { n: coursesCount }
+ )
+ }}
+ <a href="" @click.prevent="loadCourses({withoutLimit: true});">
+ {{ $gettext('Alle anzeigen') }}
+ </a>
+ </td>
+ </tr>
+ <tr v-if="!coursesLoaded">
+ <td :colspan="colspan">
+ {{ $gettext('Daten werden geladen ...') }}
+ </td>
+ </tr>
+ </tbody>
+ <tfoot v-if="buttons.bottom">
+ <tr>
+ <td v-html="buttons.bottom" style="text-align: right" :colspan="colspan"></td>
+ </tr>
+ </tfoot>
+ </table>
+ </form>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
@@ -130,6 +133,15 @@ export default {
},
created() {
this.loadCourses();
+
+ this.globalOn('AdminCourses/changeActionArea', this.changeActionArea.bind(this));
+ this.globalOn('AdminCourses/changeFilter', this.changeFilter.bind(this));
+ this.globalOn('AdminCourses/loadCourse', this.loadCourse.bind(this));
+ },
+ destroyed() {
+ this.globalOff('AdminCourses/changeActionArea', this.changeActionArea.bind(this));
+ this.globalOff('AdminCourses/changeFilter', this.changeFilter.bind(this));
+ this.globalOff('AdminCourses/loadCourse', this.loadCourse.bind(this));
},
computed: {
...mapState('admincourses', [
@@ -143,6 +155,9 @@ export default {
...mapGetters('admincourses', [
'isLoading',
]),
+ csrf() {
+ return STUDIP.CSRF_TOKEN;
+ },
colspan () {
let colspan = this.activatedFields.length + 1;
if (this.showComplete) {
@@ -284,6 +299,41 @@ export default {
getURL(url, params = {}) {
return STUDIP.URLHelper.getURL(url, params);
},
- }
+ },
};
</script>
+<style lang="scss">
+@import '../../assets/stylesheets/mixins.scss';
+
+.course-admin {
+ .course-completion {
+ @include hide-text();
+ @include square(16px);
+ background-repeat: no-repeat;
+ display: block;
+ }
+
+ th .course-completion {
+ @include background-icon(radiobutton-checked, clickable);
+ }
+
+ td .course-completion {
+ @include background-icon(span-empty, status-red);
+
+ &[data-course-completion="1"] {
+ @include background-icon(span-2quarter, status-yellow);
+ }
+ &[data-course-completion="2"] {
+ @include background-icon(span-full, status-green);
+ }
+
+ &.ajaxing {
+ background-image: url("#{$image-path}/loading-indicator.svg");
+ }
+ }
+ > tbody.loading > tr > td {
+ opacity: 0.5;
+ }
+}
+
+</style>
diff --git a/resources/vue/components/CacheAdministration.vue b/resources/vue/components/CacheAdministration.vue
index 5da9568..a7a1792 100644
--- a/resources/vue/components/CacheAdministration.vue
+++ b/resources/vue/components/CacheAdministration.vue
@@ -1,5 +1,8 @@
<template>
- <form class="default" :action="actionUrl" method="post" ref="configForm">
+ <StudipMessageBox v-if="!enabled" type="warning" :hide-close="true">
+ {{ $gettext('Caching ist systemweit ausgeschaltet, daher kann hier nichts konfiguriert werden.') }}
+ </StudipMessageBox>
+ <form v-else class="default" :action="actionUrl" method="post" ref="configForm">
<fieldset>
<legend>
<translate>Cachetyp</translate>
@@ -36,10 +39,12 @@
import FileCacheConfig from './FileCacheConfig.vue'
import MemcachedCacheConfig from './MemcachedCacheConfig.vue'
import RedisCacheConfig from './RedisCacheConfig.vue'
+import StudipMessageBox from './StudipMessageBox.vue';
export default {
name: 'CacheAdministration',
components: {
+ StudipMessageBox,
FileCacheConfig,
MemcachedCacheConfig,
RedisCacheConfig
@@ -61,6 +66,10 @@ export default {
props: []
};
}
+ },
+ enabled: {
+ type: Boolean,
+ required: true,
}
},
data () {
diff --git a/templates/header.php b/templates/header.php
index 711d517..1be47ab 100644
--- a/templates/header.php
+++ b/templates/header.php
@@ -66,14 +66,14 @@ if ($navigation) {
'username' => $user->username,
'perm' => $GLOBALS['perm']->get_perm()
];
- ?>
- <? } else {
+ } else {
$me = ['username' => 'nobody'];
} ?>
- <responsive-navigation :me="<?= htmlReady(json_encode($me)) ?>"
- context="<?= htmlReady(Context::get() ? Context::get()->getFullName() : '') ?>"
- :navigation="<?= htmlReady(json_encode(ResponsiveHelper::getNavigationObject($_COOKIE['responsive-navigation-hash'] ?? null))) ?>"
- ></responsive-navigation>
+ <?= Studip\VueApp::create('responsive/ResponsiveNavigation')->withProps([
+ 'context' => Context::get()?->getFullName() ?? '',
+ 'me' => $me,
+ 'navigation' => ResponsiveHelper::getNavigationObject($_COOKIE['responsive-navigation-hash'] ?? null),
+ ]) ?>
</div>
<div id="site-title">
<?= htmlReady(Config::get()->UNI_NAME_CLEAN) ?>
diff --git a/templates/vue-app.php b/templates/vue-app.php
new file mode 100644
index 0000000..2c34929
--- /dev/null
+++ b/templates/vue-app.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * @var array $attributes
+ * @var string $baseComponent
+ * @var array $props
+ * @var array $storeData
+ */
+?>
+<? foreach ($storeData as $store => $data): ?>
+<script type="application/json" id="vue-store-data-<?= htmlReady($store) ?>"><?= json_encode($data) ?></script>
+<? endforeach; ?>
+<div <?= arrayToHtmlAttributes($attributes) ?>>
+ <<?= strtokebabcase($baseComponent) ?> <?= arrayToHtmlAttributes($props) ?>/>
+</div>