Skip to content
Snippets Groups Projects
Commit a9c40e36 authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms
Browse files

introduce [data-vue-app], fixes #4294

Closes #4294, #4295, #4297, #4298, #4301, #4304, and #4306

Merge request studip/studip!3109
parent da240ca9
No related branches found
No related tags found
No related merge requests found
Showing
with 422 additions and 171 deletions
......@@ -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.'));
}
$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`"
),
])
);
}
/**
......
......@@ -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
......
......@@ -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,
])
);
}
/**
......
......@@ -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([
$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()
......
......@@ -113,12 +113,12 @@ 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,
......
......@@ -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()
......
<?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;
......@@ -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,
<? 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,
];
?>
<form method="post">
<?= CSRFProtection::tokenTag() ?>
<div class="admin-courses-vue-app course-admin"
is="AdminCourses"
v-cloak
ref="app"
<?= arrayToHtmlAttributes($attributes) ?>
></div>
</form>
])
->withStore('AdminCoursesStore', 'admincourses', $store_data) ?>
<? endif; ?>
......@@ -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>
......
<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>
<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>
<div class="content-modules-vue-app" is="ContentModules"></div>
......@@ -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">
......
<?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'); ?>
......
<?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>
<?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;
}
}
......@@ -588,6 +588,17 @@ abstract class StudipController extends Trails\Controller
$this->render_text($form->render());
}
/**
* 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
......
<?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();
}
}
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');
......
/**
* 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 }
})
})
}
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment