mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 06:54:04 +00:00
WIP
This commit is contained in:
@@ -61,6 +61,12 @@ $route['api/v1/organisation/[O|o]rganisationseinheit/(:any)'] = 'api/v1/organisa
|
||||
$route['api/v1/ressource/[B|b]etriebsmittelperson/(:any)'] = 'api/v1/ressource/betriebsmittelperson2/$1';
|
||||
$route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1';
|
||||
|
||||
// Legacy CMS news routes
|
||||
$route['api/frontend/v1/Cms/getNewsItem/(:num)'] = 'api/frontend/v1/NewsAdministrationAPI/getNewsItem/$1';
|
||||
$route['api/frontend/v1/Cms/storeNewsItem'] = 'api/frontend/v1/NewsAdministrationAPI/storeNewsItem';
|
||||
$route['api/frontend/v1/Cms/updateNewsItem/(:num)'] = 'api/frontend/v1/NewsAdministrationAPI/updateNewsItem/$1';
|
||||
$route['api/frontend/v1/Cms/deleteNewsItem/(:num)'] = 'api/frontend/v1/NewsAdministrationAPI/deleteNewsItem/$1';
|
||||
|
||||
$route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1';
|
||||
$route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1';
|
||||
$route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1';
|
||||
|
||||
@@ -17,9 +17,9 @@ class Cms extends Auth_Controller
|
||||
array(
|
||||
'content' => 'basis/cis:r',
|
||||
'getNews' => 'basis/cis:r',
|
||||
'getNewsRowCount' => 'basis/cis:r',
|
||||
'getRoomInformation' => 'basis/cis:r',
|
||||
'news' => 'basis/cis:r'
|
||||
'news' => 'basis/cis:r',
|
||||
'newsAdministration' => ['basis/news:r', 'basis/cis:r'],
|
||||
)
|
||||
);
|
||||
|
||||
@@ -87,6 +87,12 @@ class Cms extends Auth_Controller
|
||||
$this->load->view('CisRouterView/CisRouterView.php', ['viewData'=>$viewData, 'route' => 'News']);
|
||||
}
|
||||
|
||||
public function newsAdministration()
|
||||
{
|
||||
$viewData = array();
|
||||
$this->load->view('CisRouterView/CisRouterView.php', ['viewData'=>$viewData, 'route' => 'NewsAdministration']);
|
||||
}
|
||||
|
||||
public function getRoomInformation($ort_kurzbz)
|
||||
{
|
||||
// Load Config
|
||||
|
||||
@@ -36,13 +36,13 @@ class Cms extends FHCAPI_Controller
|
||||
'ContentID' => self::PERM_LOGGED,
|
||||
'getOrtKurzbzContent' => self::PERM_LOGGED,
|
||||
'content' => self::PERM_LOGGED,
|
||||
'news' => self::PERM_LOGGED,
|
||||
'getNewsRowCount' => self::PERM_LOGGED,
|
||||
'newsForWidget' => self::PERM_LOGGED,
|
||||
'getNews' => self::PERM_LOGGED,
|
||||
|
||||
]);
|
||||
|
||||
$this->load->model('content/News_model', 'NewsModel');
|
||||
$this->load->model('crm/Student_model', 'StudentModel');
|
||||
|
||||
// setting up the papgination_size
|
||||
$this->page_size = 10;
|
||||
@@ -59,7 +59,6 @@ class Cms extends FHCAPI_Controller
|
||||
//------------------------------------------------------------------------------------------------------------------
|
||||
// Private methods
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------------------------------------------
|
||||
// Public methods
|
||||
|
||||
@@ -106,7 +105,7 @@ class Cms extends FHCAPI_Controller
|
||||
$this->terminateWithSuccess($content_id);
|
||||
}
|
||||
|
||||
public function news()
|
||||
public function newsForWidget()
|
||||
{
|
||||
|
||||
// form validation
|
||||
@@ -162,28 +161,6 @@ class Cms extends FHCAPI_Controller
|
||||
|
||||
}
|
||||
|
||||
public function getNewsRowCount($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $fachbereich_kurzbz = null, $maxalter = 0, $edit = false, $sichtbar = true, $page = 1, $page_size = 10)
|
||||
{
|
||||
list($studiengang_kz, $semester) = $this->cmslib->getStgAndSem($studiengang_kz, $semester);
|
||||
$all = $edit;
|
||||
|
||||
$this->load->model('content/News_model','NewsModel');
|
||||
|
||||
$passedInMaxAlter = $this->input->get('maxAlter', true);
|
||||
if($passedInMaxAlter !== null)
|
||||
{
|
||||
$maxAlter = intval($passedInMaxAlter);
|
||||
}
|
||||
|
||||
$num_rows = $this->NewsModel->countNewsWithContent(getSprache(), $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxAlter, $page, $page_size, $all, $mischen);
|
||||
|
||||
$num_rows = $this->getDataOrTerminateWithError($num_rows);
|
||||
|
||||
$this->terminateWithSuccess($num_rows);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $maxAlter = 0)
|
||||
{
|
||||
//form validation
|
||||
@@ -210,15 +187,34 @@ class Cms extends FHCAPI_Controller
|
||||
{
|
||||
$maxAlter = intval($passedInMaxAlter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$passedSichtbar = $this->input->get('published', true);
|
||||
if($passedSichtbar !== null)
|
||||
{
|
||||
$sichtbar = $passedSichtbar;
|
||||
}
|
||||
|
||||
$student = $this->StudentModel->loadWhere(['student_uid' => getAuthUID()]);
|
||||
if(isError($student))
|
||||
{
|
||||
$this->terminateWithError(getError($student));
|
||||
}
|
||||
if(hasData($student))
|
||||
{
|
||||
$student = current(getData($student));
|
||||
$studiengang_kz = $student->studiengang_kz;
|
||||
$semester = $student->semester;
|
||||
$this->addMeta('semester', $semester);
|
||||
}
|
||||
|
||||
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size, $sprache, $maxAlter);
|
||||
$news = $this->getDataOrTerminateWithError($news);
|
||||
|
||||
$this->addMeta('phrases', json_decode($this->p->getJson()));
|
||||
$this->terminateWithSuccess($news);
|
||||
$this->addMeta('row_count', $news["full_count"] ?? 0);
|
||||
$this->terminateWithSuccess($news["content"]);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,815 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2026 fhcomplete.org
|
||||
*
|
||||
* 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
if (!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
class NewsAdministrationAPI extends FHCAPI_Controller
|
||||
{
|
||||
const GENERAL_DEGREE_PROGRAM_ID = 0;
|
||||
/**
|
||||
* News API constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'getNews' => 'basis/news:r',
|
||||
'getNewsItem' => 'basis/news:r',
|
||||
'storeNewsItem' => 'basis/news:w',
|
||||
'updateNewsItem' => 'basis/news:w',
|
||||
'deleteNewsItem' => 'basis/news:w'
|
||||
]);
|
||||
|
||||
$this->load->model('content/Content_model', 'ContentModel');
|
||||
$this->load->model('content/Contentsprache_model', 'ContentspracheModel');
|
||||
$this->load->model('content/News_model', 'NewsModel');
|
||||
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
|
||||
$this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
|
||||
$this->load->model('system/Sprache_model', 'SpracheModel');
|
||||
|
||||
$this->loadPhrases([
|
||||
'global'
|
||||
]);
|
||||
|
||||
$this->load->library('CmsLib');
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------------------------------------
|
||||
// Public methods
|
||||
|
||||
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = true, $sichtbar = true, $maxAlter = 0)
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('page','Page','required|is_natural');
|
||||
$this->form_validation->set_rules('page_size', 'PageSize', 'is_natural');
|
||||
if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$page = intval($this->input->get('page', true));
|
||||
$page_size = intval($this->input->get('page_size', true));
|
||||
$sprache = $this->input->get('sprache', true);
|
||||
if(!$sprache)
|
||||
{
|
||||
$sprache = getUserLanguage();
|
||||
}
|
||||
|
||||
// default value for the page_size is 10
|
||||
$page_size = $page_size ?? 10;
|
||||
|
||||
$passedInMaxAlter = $this->input->get('maxAlter', true);
|
||||
if($passedInMaxAlter !== null)
|
||||
{
|
||||
$maxAlter = intval($passedInMaxAlter);
|
||||
}
|
||||
|
||||
|
||||
$passedSichtbar = $this->input->get('published', true);
|
||||
if($passedSichtbar !== null)
|
||||
{
|
||||
$sichtbar = $passedSichtbar;
|
||||
}
|
||||
|
||||
$allowedDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
|
||||
array_push($allowedDegreePrograms, self::GENERAL_DEGREE_PROGRAM_ID);
|
||||
|
||||
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size, $sprache, $maxAlter, true, $allowedDegreePrograms);
|
||||
$news = $this->getDataOrTerminateWithError($news);
|
||||
|
||||
$this->addMeta('phrases', json_decode($this->p->getJson()));
|
||||
|
||||
$this->addMeta('row_count', $news["full_count"] ?? 0);
|
||||
$this->terminateWithSuccess($news["content"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one news entry with all available language versions for editing.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getNewsItem($newsId)
|
||||
{
|
||||
if (!is_numeric($newsId) || (int)$newsId < 1)
|
||||
{
|
||||
$this->terminateWithValidationErrors([
|
||||
'newsId' => 'A valid news ID is required'
|
||||
]);
|
||||
}
|
||||
|
||||
$newsId = (int)$newsId;
|
||||
$newsResult = $this->NewsModel->load($newsId);
|
||||
|
||||
if (isError($newsResult))
|
||||
{
|
||||
$this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
if (!hasData($newsResult))
|
||||
{
|
||||
$this->terminateWithValidationErrors([
|
||||
'newsId' => 'The requested news entry does not exist'
|
||||
]);
|
||||
}
|
||||
|
||||
$news = current(getData($newsResult));
|
||||
$allowedDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
|
||||
array_push($allowedDegreePrograms, self::GENERAL_DEGREE_PROGRAM_ID);
|
||||
|
||||
if (!in_array($news->studiengang_kz, $allowedDegreePrograms)) {
|
||||
$this->terminateWithError('You are not entitled to access this news entry', self::ERROR_TYPE_PERMISSION);
|
||||
}
|
||||
$translationsResult = $this->ContentspracheModel->loadWhere([
|
||||
'content_id' => $news->content_id
|
||||
]);
|
||||
|
||||
if (isError($translationsResult))
|
||||
{
|
||||
$this->terminateWithError(getError($translationsResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$languages = [];
|
||||
|
||||
foreach ((array)getData($translationsResult) as $translation)
|
||||
{
|
||||
$languages[$translation->sprache] = true;
|
||||
}
|
||||
|
||||
$translations = [];
|
||||
|
||||
foreach (array_keys($languages) as $language)
|
||||
{
|
||||
$contentResult = $this->ContentModel->getContent(
|
||||
$news->content_id,
|
||||
$language,
|
||||
null,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
if (isError($contentResult))
|
||||
{
|
||||
$this->terminateWithError(getError($contentResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$content = getData($contentResult);
|
||||
$document = new DOMDocument('1.0', 'UTF-8');
|
||||
|
||||
if ($content->content !== '' && $document->loadXML($content->content) === false)
|
||||
{
|
||||
$this->terminateWithError(
|
||||
'The stored news content is invalid',
|
||||
self::ERROR_TYPE_GENERAL
|
||||
);
|
||||
}
|
||||
|
||||
$author = $document->getElementsByTagName('verfasser')->item(0);
|
||||
$title = $document->getElementsByTagName('betreff')->item(0);
|
||||
$text = $document->getElementsByTagName('text')->item(0);
|
||||
|
||||
$translations[] = [
|
||||
'language' => $language,
|
||||
'author' => $author ? $author->nodeValue : '',
|
||||
'title' => $title ? $title->nodeValue : '',
|
||||
'text' => $text ? $text->nodeValue : '',
|
||||
'isPublished' => (bool)$content->sichtbar
|
||||
];
|
||||
}
|
||||
|
||||
$this->terminateWithSuccess([
|
||||
'newsId' => (int)$news->news_id,
|
||||
'visibleFrom' => $news->datum,
|
||||
'visibleTo' => $news->datum_bis,
|
||||
'degreeProgramShortCode' => $news->studiengang_kz,
|
||||
'semester' => $news->semester,
|
||||
'translations' => $translations
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a news entry and all supplied translations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function storeNewsItem()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$postData = $this->input->post(null, true);
|
||||
$this->form_validation->set_data(is_array($postData) ? $postData : []);
|
||||
$this->form_validation->set_rules('visibleFrom', 'Visible from', 'required|is_valid_date');
|
||||
$this->form_validation->set_rules('visibleTo', 'Visible to', 'is_valid_date');
|
||||
$this->form_validation->set_rules(
|
||||
'degreeProgramShortCode',
|
||||
'Degree program',
|
||||
'required|is_natural'
|
||||
);
|
||||
$this->form_validation->set_rules(
|
||||
'semester',
|
||||
'Semester',
|
||||
'is_natural_no_zero|less_than_equal_to[8]'
|
||||
);
|
||||
|
||||
$errors = $this->form_validation->run() === false
|
||||
? $this->form_validation->error_array()
|
||||
: [];
|
||||
$visibleFrom = $this->input->post('visibleFrom', true);
|
||||
$visibleTo = $this->input->post('visibleTo', true);
|
||||
$translations = $this->input->post('translations', true);
|
||||
|
||||
if (
|
||||
is_string($visibleFrom)
|
||||
&& is_string($visibleTo)
|
||||
&& $visibleTo !== ''
|
||||
&& is_valid_date($visibleFrom)
|
||||
&& is_valid_date($visibleTo)
|
||||
&& new DateTime($visibleTo) < new DateTime($visibleFrom)
|
||||
)
|
||||
{
|
||||
$errors['visibleTo'] = 'Visible to must not be before Visible from';
|
||||
}
|
||||
|
||||
if (!is_array($translations) || count($translations) === 0)
|
||||
{
|
||||
$errors['translations'] = 'At least one translation is required';
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($translations as $index => $translation)
|
||||
{
|
||||
if (!is_array($translation))
|
||||
{
|
||||
$errors['translations.' . $index] = 'Each translation must be an object';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($errors) > 0)
|
||||
{
|
||||
$this->terminateWithValidationErrors($errors);
|
||||
}
|
||||
|
||||
|
||||
$this->_ensureUserIsEntitledForDegreeProgram($this->input->post('degreeProgramShortCode', true));
|
||||
|
||||
$degreeProgramShortCode = $this->input->post('degreeProgramShortCode', true);
|
||||
$semester = $this->input->post('semester', true);
|
||||
$semester = $semester === '' ? null : $semester;
|
||||
$visibleTo = $visibleTo === '' ? null : $visibleTo;
|
||||
|
||||
$studiengang = $this->_getStudiengang($degreeProgramShortCode);
|
||||
|
||||
$data = [
|
||||
'studiengang_kz' => $degreeProgramShortCode,
|
||||
'semester' => $semester,
|
||||
'datum' => $visibleFrom,
|
||||
'datum_bis' => $visibleTo,
|
||||
'translations' => []
|
||||
];
|
||||
|
||||
foreach ($translations as $index => $translation)
|
||||
{
|
||||
$data['translations'][$index] = [
|
||||
'sprache' => $translation['language'] ?? '',
|
||||
'verfasser' => $translation['author'] ?? '',
|
||||
'betreff' => $translation['title'] ?? '',
|
||||
'text' => $translation['text'] ?? '',
|
||||
'sichtbar' => isset($translation['isPublished']) ? (bool)$translation['isPublished'] : false
|
||||
];
|
||||
}
|
||||
|
||||
$this->_validateLanguages($data['translations']);
|
||||
|
||||
foreach ($data['translations'] as $index => $translation)
|
||||
{
|
||||
$data['translations'][$index]['content'] = $this->_buildNewsXml($translation);
|
||||
}
|
||||
|
||||
$uid = getAuthUID();
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
if ($this->db->trans_begin() === false)
|
||||
{
|
||||
$this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$contentResult = $this->ContentModel->insert([
|
||||
'template_kurzbz' => 'news',
|
||||
'oe_kurzbz' => $studiengang->oe_kurzbz,
|
||||
'aktiv' => true,
|
||||
'menu_open' => false,
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid
|
||||
]);
|
||||
$contentId = (int)$this->_getInsertedIdOrRollback($contentResult);
|
||||
|
||||
$newsResult = $this->NewsModel->insert([
|
||||
'uid' => $uid,
|
||||
'studiengang_kz' => $data['studiengang_kz'],
|
||||
'semester' => $data['semester'],
|
||||
'datum' => $data['datum'],
|
||||
'datum_bis' => $data['datum_bis'],
|
||||
'content_id' => $contentId,
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid,
|
||||
'updateamum' => $now,
|
||||
'updatevon' => $uid
|
||||
]);
|
||||
$newsId = (int)$this->_getInsertedIdOrRollback($newsResult);
|
||||
|
||||
foreach ($data['translations'] as $translation)
|
||||
{
|
||||
$translationResult = $this->ContentspracheModel->insert([
|
||||
'sprache' => $translation['sprache'],
|
||||
'content_id' => $contentId,
|
||||
'version' => 1,
|
||||
'sichtbar' => $translation['sichtbar'],
|
||||
'content' => $translation['content'],
|
||||
'titel' => $translation['betreff'],
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid,
|
||||
'updateamum' => $now,
|
||||
'updatevon' => $uid
|
||||
]);
|
||||
|
||||
$this->_getInsertedIdOrRollback($translationResult);
|
||||
}
|
||||
|
||||
if ($this->db->trans_status() === false)
|
||||
{
|
||||
$error = $this->db->error();
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError($error, self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
if ($this->db->trans_commit() === false)
|
||||
{
|
||||
$error = $this->db->error();
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError($error, self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$this->terminateWithSuccess($newsId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a news entry and creates new versions of its supplied translations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateNewsItem($newsId)
|
||||
{
|
||||
if (!is_numeric($newsId) || (int)$newsId < 1)
|
||||
{
|
||||
$this->terminateWithValidationErrors([
|
||||
'newsId' => 'A valid news ID is required'
|
||||
]);
|
||||
}
|
||||
|
||||
$newsId = (int)$newsId;
|
||||
$existingNewsResult = $this->NewsModel->load($newsId);
|
||||
|
||||
if (isError($existingNewsResult))
|
||||
{
|
||||
$this->terminateWithError(getError($existingNewsResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
if (!hasData($existingNewsResult))
|
||||
{
|
||||
$this->terminateWithValidationErrors([
|
||||
'newsId' => 'The requested news entry does not exist'
|
||||
]);
|
||||
}
|
||||
|
||||
$existingNews = current(getData($existingNewsResult));
|
||||
|
||||
$this->_ensureUserIsEntitledForDegreeProgram($existingNews->studiengang_kz);
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$postData = $this->input->post(null, true);
|
||||
$this->form_validation->set_data(is_array($postData) ? $postData : []);
|
||||
$this->form_validation->set_rules('visibleFrom', 'Visible from', 'required|is_valid_date');
|
||||
$this->form_validation->set_rules('visibleTo', 'Visible to', 'is_valid_date');
|
||||
$this->form_validation->set_rules(
|
||||
'degreeProgramShortCode',
|
||||
'Degree program',
|
||||
'required|is_natural'
|
||||
);
|
||||
$this->form_validation->set_rules(
|
||||
'semester',
|
||||
'Semester',
|
||||
'is_natural_no_zero|less_than_equal_to[8]'
|
||||
);
|
||||
|
||||
$errors = $this->form_validation->run() === false
|
||||
? $this->form_validation->error_array()
|
||||
: [];
|
||||
$visibleFrom = $this->input->post('visibleFrom', true);
|
||||
$visibleTo = $this->input->post('visibleTo', true);
|
||||
$translations = $this->input->post('translations', true);
|
||||
|
||||
if (
|
||||
is_string($visibleFrom)
|
||||
&& is_string($visibleTo)
|
||||
&& $visibleTo !== ''
|
||||
&& is_valid_date($visibleFrom)
|
||||
&& is_valid_date($visibleTo)
|
||||
&& new DateTime($visibleTo) < new DateTime($visibleFrom)
|
||||
)
|
||||
{
|
||||
$errors['visibleTo'] = 'Visible to must not be before Visible from';
|
||||
}
|
||||
|
||||
if (!is_array($translations) || count($translations) === 0)
|
||||
{
|
||||
$errors['translations'] = 'At least one translation is required';
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ($translations as $index => $translation)
|
||||
{
|
||||
if (!is_array($translation))
|
||||
{
|
||||
$errors['translations.' . $index] = 'Each translation must be an object';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($errors) > 0)
|
||||
{
|
||||
$this->terminateWithValidationErrors($errors);
|
||||
}
|
||||
|
||||
$degreeProgramShortCode = $this->input->post('degreeProgramShortCode', true);
|
||||
$semester = $this->input->post('semester', true);
|
||||
$semester = $semester === '' ? null : $semester;
|
||||
$visibleTo = $visibleTo === '' ? null : $visibleTo;
|
||||
|
||||
$studiengang = $this->_getStudiengang($degreeProgramShortCode);
|
||||
|
||||
$data = [
|
||||
'studiengang_kz' => $degreeProgramShortCode,
|
||||
'semester' => $semester,
|
||||
'datum' => $visibleFrom,
|
||||
'datum_bis' => $visibleTo,
|
||||
'translations' => []
|
||||
];
|
||||
|
||||
foreach ($translations as $index => $translation)
|
||||
{
|
||||
$data['translations'][$index] = [
|
||||
'sprache' => $translation['language'] ?? '',
|
||||
'verfasser' => $translation['author'] ?? '',
|
||||
'betreff' => $translation['title'] ?? '',
|
||||
'text' => $translation['text'] ?? '',
|
||||
'sichtbar' => isset($translation['isPublished']) ? (bool)$translation['isPublished'] : false
|
||||
];
|
||||
}
|
||||
|
||||
$this->_validateLanguages($data['translations']);
|
||||
|
||||
foreach ($data['translations'] as $index => $translation)
|
||||
{
|
||||
$data['translations'][$index]['content'] = $this->_buildNewsXml($translation);
|
||||
}
|
||||
|
||||
$uid = getAuthUID();
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
if ($this->db->trans_begin() === false)
|
||||
{
|
||||
$this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$contentResult = $this->ContentModel->update($existingNews->content_id, [
|
||||
'oe_kurzbz' => $studiengang->oe_kurzbz,
|
||||
'updateamum' => $now,
|
||||
'updatevon' => $uid
|
||||
]);
|
||||
|
||||
if (isError($contentResult))
|
||||
{
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError(getError($contentResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$newsResult = $this->NewsModel->update($newsId, [
|
||||
'studiengang_kz' => $data['studiengang_kz'],
|
||||
'semester' => $data['semester'],
|
||||
'datum' => $data['datum'],
|
||||
'datum_bis' => $data['datum_bis'],
|
||||
'updateamum' => $now,
|
||||
'updatevon' => $uid
|
||||
]);
|
||||
|
||||
if (isError($newsResult))
|
||||
{
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$existingTranslationsResult = $this->ContentspracheModel->loadWhere([
|
||||
'content_id' => $existingNews->content_id
|
||||
]);
|
||||
|
||||
if (isError($existingTranslationsResult))
|
||||
{
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError(getError($existingTranslationsResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$latestVersions = [];
|
||||
|
||||
foreach ((array)getData($existingTranslationsResult) as $existingTranslation)
|
||||
{
|
||||
$language = $existingTranslation->sprache;
|
||||
$version = (int)$existingTranslation->version;
|
||||
$latestVersions[$language] = max($latestVersions[$language] ?? 0, $version);
|
||||
}
|
||||
|
||||
foreach ($data['translations'] as $translation)
|
||||
{
|
||||
$translationResult = $this->ContentspracheModel->insert([
|
||||
'sprache' => $translation['sprache'],
|
||||
'content_id' => $existingNews->content_id,
|
||||
'version' => ($latestVersions[$translation['sprache']] ?? 0) + 1,
|
||||
'sichtbar' => $translation['sichtbar'],
|
||||
'content' => $translation['content'],
|
||||
'titel' => $translation['betreff'],
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid,
|
||||
'updateamum' => $now,
|
||||
'updatevon' => $uid
|
||||
]);
|
||||
|
||||
$this->_getInsertedIdOrRollback($translationResult);
|
||||
}
|
||||
|
||||
if ($this->db->trans_status() === false)
|
||||
{
|
||||
$error = $this->db->error();
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError($error, self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
if ($this->db->trans_commit() === false)
|
||||
{
|
||||
$error = $this->db->error();
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError($error, self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$this->terminateWithSuccess($newsId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes one news entry and its linked content.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteNewsItem($newsId)
|
||||
{
|
||||
if (!is_numeric($newsId) || (int)$newsId < 1)
|
||||
{
|
||||
$this->terminateWithValidationErrors([
|
||||
'newsId' => 'A valid news ID is required'
|
||||
]);
|
||||
}
|
||||
|
||||
$newsId = (int)$newsId;
|
||||
$newsResult = $this->NewsModel->load($newsId);
|
||||
|
||||
if (isError($newsResult))
|
||||
{
|
||||
$this->terminateWithError(getError($newsResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
if (!hasData($newsResult))
|
||||
{
|
||||
$this->terminateWithValidationErrors([
|
||||
'newsId' => 'The requested news entry does not exist'
|
||||
]);
|
||||
}
|
||||
|
||||
$news = current(getData($newsResult));
|
||||
|
||||
|
||||
$this->_ensureUserIsEntitledForDegreeProgram($news->studiengang_kz);
|
||||
|
||||
if ($this->db->trans_begin() === false)
|
||||
{
|
||||
$this->terminateWithError($this->db->error(), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$newsDeleteResult = $this->NewsModel->delete($newsId);
|
||||
|
||||
if (isError($newsDeleteResult))
|
||||
{
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError(getError($newsDeleteResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$this->db->where('content_id', $news->content_id);
|
||||
|
||||
if ($this->db->delete('campus.tbl_contentsprache') === false)
|
||||
{
|
||||
$error = $this->db->error();
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError($error, self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$contentDeleteResult = $this->ContentModel->delete($news->content_id);
|
||||
|
||||
if (isError($contentDeleteResult))
|
||||
{
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError(getError($contentDeleteResult), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
if ($this->db->trans_status() === false)
|
||||
{
|
||||
$error = $this->db->error();
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError($error, self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
if ($this->db->trans_commit() === false)
|
||||
{
|
||||
$error = $this->db->error();
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError($error, self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$this->terminateWithSuccess($newsId);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------------------------------------
|
||||
// Private methods
|
||||
|
||||
/**
|
||||
* Loads the requested Studiengang or reports a validation error.
|
||||
*
|
||||
* @param integer $studiengangKz
|
||||
* @return stdClass
|
||||
*/
|
||||
private function _getStudiengang($studiengangKz)
|
||||
{
|
||||
$result = $this->StudiengangModel->load($studiengangKz);
|
||||
|
||||
if (isError($result))
|
||||
{
|
||||
$this->terminateWithError(getError($result), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
if (!hasData($result))
|
||||
{
|
||||
$this->terminateWithValidationErrors([
|
||||
'studiengang_kz' => 'The requested Studiengang does not exist'
|
||||
]);
|
||||
}
|
||||
|
||||
$studiengaenge = getData($result);
|
||||
|
||||
return current($studiengaenge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that all supplied languages exist in tbl_sprache.
|
||||
*
|
||||
* @param array $translations
|
||||
* @return void
|
||||
*/
|
||||
private function _validateLanguages($translations)
|
||||
{
|
||||
$languages = [];
|
||||
|
||||
foreach ($translations as $translation)
|
||||
{
|
||||
$languages[] = $translation['sprache'];
|
||||
}
|
||||
|
||||
$result = $this->SpracheModel->loadMultiple($languages);
|
||||
|
||||
if (isError($result))
|
||||
{
|
||||
$this->terminateWithError(getError($result), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
$existingLanguages = [];
|
||||
|
||||
foreach ((array)getData($result) as $language)
|
||||
{
|
||||
$existingLanguages[$language->sprache] = true;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach ($translations as $index => $translation)
|
||||
{
|
||||
if (!isset($existingLanguages[$translation['sprache']]))
|
||||
{
|
||||
$errors['translations.' . $index . '.sprache'] = 'The requested language does not exist';
|
||||
}
|
||||
}
|
||||
|
||||
if (count($errors) > 0)
|
||||
{
|
||||
$this->terminateWithValidationErrors($errors);
|
||||
}
|
||||
}
|
||||
|
||||
private function _ensureUserIsEntitledForDegreeProgram($degreeProgramShortCode)
|
||||
{
|
||||
$entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news:w");
|
||||
if (!is_array($entitledDegreePrograms))
|
||||
{
|
||||
$this->terminateWithError('Failed to retrieve entitled degree programs for the user', self::ERROR_TYPE_GENERAL);
|
||||
}
|
||||
|
||||
if (!in_array($degreeProgramShortCode, $entitledDegreePrograms)) {
|
||||
$this->terminateWithError("User is not entitled for the specified degree program");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns an inserted identifier or rolls the transaction back on error.
|
||||
*
|
||||
* @param stdClass $result
|
||||
* @return mixed
|
||||
*/
|
||||
private function _getInsertedIdOrRollback($result)
|
||||
{
|
||||
if (isError($result))
|
||||
{
|
||||
$this->db->trans_rollback();
|
||||
$this->terminateWithError(getError($result), self::ERROR_TYPE_DB);
|
||||
}
|
||||
|
||||
return getData($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the XML format consumed by existing news readers.
|
||||
*
|
||||
* @param array $translation
|
||||
* @return string
|
||||
*/
|
||||
private function _buildNewsXml($translation)
|
||||
{
|
||||
$document = new DOMDocument('1.0', 'UTF-8');
|
||||
$news = $document->createElement('news');
|
||||
$document->appendChild($news);
|
||||
|
||||
$this->_appendCdataElement($document, $news, 'verfasser', $translation['verfasser']);
|
||||
$this->_appendCdataElement($document, $news, 'betreff', $translation['betreff']);
|
||||
$this->_appendCdataElement($document, $news, 'text', $translation['text']);
|
||||
|
||||
return $document->saveXML($news);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a CDATA element and safely preserves CDATA terminators.
|
||||
*
|
||||
* @param DOMDocument $document
|
||||
* @param DOMElement $parent
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
private function _appendCdataElement($document, $parent, $name, $value)
|
||||
{
|
||||
$element = $document->createElement($name);
|
||||
$parts = explode(']]>', $value);
|
||||
$lastIndex = count($parts) - 1;
|
||||
|
||||
foreach ($parts as $index => $part)
|
||||
{
|
||||
if ($index > 0)
|
||||
{
|
||||
$part = '>' . $part;
|
||||
}
|
||||
|
||||
if ($index < $lastIndex)
|
||||
{
|
||||
$part .= ']]';
|
||||
}
|
||||
|
||||
$element->appendChild($document->createCDATASection($part));
|
||||
}
|
||||
|
||||
$parent->appendChild($element);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class Studgang extends FHCAPI_Controller
|
||||
{
|
||||
parent::__construct([
|
||||
'getStudiengangInfo'=> self::PERM_LOGGED,
|
||||
|
||||
'getDegreePrograms'=> self::PERM_LOGGED,
|
||||
]);
|
||||
|
||||
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
|
||||
@@ -43,6 +43,22 @@ class Studgang extends FHCAPI_Controller
|
||||
//------------------------------------------------------------------------------------------------------------------
|
||||
// Public methods
|
||||
|
||||
public function getDegreePrograms() {
|
||||
$entitledDegreePrograms = $this->permissionlib->getSTG_isEntitledFor("basis/news");
|
||||
if (!$entitledDegreePrograms) {
|
||||
$this->terminateWithError("User is not entitled for any degree program");
|
||||
}
|
||||
|
||||
$activeDegreePrograms = $this->StudiengangModel->getDegreePrograms();
|
||||
$activeDegreePrograms= $this->getDataOrTerminateWithError($activeDegreePrograms);
|
||||
|
||||
$activeDegreePrograms = array_values(array_filter($activeDegreePrograms, function($degreeProgram) use ($entitledDegreePrograms) {
|
||||
return in_array($degreeProgram->studiengang_kz, $entitledDegreePrograms);
|
||||
}));
|
||||
|
||||
$this->terminateWithSuccess($activeDegreePrograms);
|
||||
}
|
||||
|
||||
public function getStudiengangInfo(){
|
||||
$isMitarbeiter = $this->MitarbeiterModel->isMitarbeiter(getAuthUID());
|
||||
$isMitarbeiter = $this->getDataOrTerminateWithError($isMitarbeiter);
|
||||
|
||||
@@ -141,63 +141,6 @@ class CmsLib
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stdClass $stg_obj
|
||||
*
|
||||
* @return stdClass
|
||||
*/
|
||||
protected function getNewsExtras($stg_obj, $semester)
|
||||
{
|
||||
$this->ci->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel');
|
||||
|
||||
$stg_ltg = $this->ci->StudiengangModel->getLeitungDetailed($stg_obj->studiengang_kz);
|
||||
if (isError($stg_ltg))
|
||||
return $stg_ltg;
|
||||
$stg_ltg = getData($stg_ltg) ?: [];
|
||||
|
||||
$gf_ltg = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('gLtg', $stg_obj->oe_kurzbz);
|
||||
if (isError($gf_ltg))
|
||||
return $gf_ltg;
|
||||
$gf_ltg = getData($gf_ltg) ?: [];
|
||||
|
||||
$stv_ltg = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('stvLtg', $stg_obj->oe_kurzbz);
|
||||
if (isError($stv_ltg))
|
||||
return $stv_ltg;
|
||||
$stv_ltg = getData($stv_ltg) ?: [];
|
||||
|
||||
$ass = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('ass', $stg_obj->oe_kurzbz);
|
||||
if (isError($ass))
|
||||
return $ass;
|
||||
$ass = getData($ass) ?: [];
|
||||
|
||||
$hochschulvertr = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('hsv');
|
||||
if (isError($hochschulvertr))
|
||||
return $hochschulvertr;
|
||||
$hochschulvertr = getData($hochschulvertr) ?: [];
|
||||
|
||||
$stdv = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('stdv', $stg_obj->oe_kurzbz);
|
||||
if (isError($stdv))
|
||||
return $stdv;
|
||||
$stdv = getData($stdv) ?: [];
|
||||
|
||||
$jahrgangsvertr = $this->ci->BenutzerfunktionModel->getBenutzerFunktionenDetailed('jgv', $stg_obj->oe_kurzbz, $semester);
|
||||
if (isError($jahrgangsvertr))
|
||||
return $jahrgangsvertr;
|
||||
$jahrgangsvertr = getData($jahrgangsvertr) ?: [];
|
||||
|
||||
return success($this->ci->load->view('Cis/Cms/News/Xml/NewsExtras', [
|
||||
'studiengang' => $stg_obj,
|
||||
'semester' => $semester,
|
||||
'stg_ltg' => $stg_ltg,
|
||||
'gf_ltg' => $gf_ltg,
|
||||
'stv_ltg' => $stv_ltg,
|
||||
'ass' => $ass,
|
||||
'hochschulvertr' => $hochschulvertr,
|
||||
'stdv' => $stdv,
|
||||
'jahrgangsvertr' => $jahrgangsvertr
|
||||
], true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $studiengang_kz
|
||||
* @param string $semester
|
||||
@@ -233,22 +176,22 @@ class CmsLib
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $page = 1, $page_size = 10, $sprache, $maxAlter = 0)
|
||||
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $page = 1, $page_size = 10, $sprache, $maxAlter = 0, $filterForDegreePrograms = false, $allowedDegreePrograms = [])
|
||||
|
||||
{
|
||||
$this->ci->load->model('organisation/Studiengang_model', 'StudiengangModel');
|
||||
list($studiengang_kz, $semester) = $this->getStgAndSem($studiengang_kz, $semester);
|
||||
log_message('error', 'cmslib getNews Studiengang: ' . $studiengang_kz . ', Semester: ' . $semester);
|
||||
$all = $edit;
|
||||
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><content>';
|
||||
|
||||
$this->ci->load->model('content/News_model', 'NewsModel');
|
||||
$news = $this->ci->NewsModel->getNewsWithContent($sprache, $studiengang_kz, $semester, null, $sichtbar, $maxAlter, $page, $page_size, $all, $mischen);
|
||||
|
||||
if (isError($news))
|
||||
return $news;
|
||||
|
||||
$news = getData($news);
|
||||
|
||||
|
||||
$news = $this->ci->NewsModel->getNewsWithContent($sprache, $studiengang_kz, $semester, null, $sichtbar, $maxAlter, $page, $page_size, $all, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
|
||||
$news = getData($news) ?? [];
|
||||
|
||||
foreach ($news as $newsobj) {
|
||||
if ($studiengang_kz && $edit && !$newsobj->studiengang_kz)
|
||||
continue;
|
||||
@@ -256,26 +199,10 @@ class CmsLib
|
||||
$datum = '<datum><![CDATA[' . $date->format('d.m.Y') . ']]></datum>';
|
||||
$datum .= '<datumdetail><![CDATA[' . $date->format('Y-m-d H:i') . ']]></datumdetail>';
|
||||
$id = $edit ? '<news_id><![CDATA[' . $newsobj->news_id . ']]></news_id>' : '';
|
||||
$xml .= "<newswrapper>" . $newsobj->content . $datum . $id . "</newswrapper>";
|
||||
$isPublished = $edit ? '<is_published><![CDATA[' . ($newsobj->sichtbar ? 'true' : 'false') . ']]></is_published>' : '';
|
||||
$xml .= "<newswrapper>" . $newsobj->content . $datum . $id . $isPublished . "</newswrapper>";
|
||||
}
|
||||
|
||||
/* if ($studiengang_kz != 0) {
|
||||
$stg_obj = $this->ci->StudiengangModel->load($studiengang_kz);
|
||||
if (isError($stg_obj))
|
||||
return $stg_obj;
|
||||
$stg_obj = current(getData($stg_obj) ?: []);
|
||||
|
||||
if ($stg_obj) {
|
||||
if (!$edit && !$infoscreen) {
|
||||
$extras = $this->getNewsExtras($stg_obj, $semester);
|
||||
if (isError($extras))
|
||||
return $extras;
|
||||
$xml .= getData($extras);
|
||||
}
|
||||
$xml .= '<studiengang_bezeichnung><![CDATA[' . $stg_obj->bezeichnung . ']]></studiengang_bezeichnung>';
|
||||
}
|
||||
} */
|
||||
|
||||
if ($titel != '') {
|
||||
$xml .= '<news_titel>' . $titel . '</news_titel>';
|
||||
}
|
||||
@@ -304,6 +231,9 @@ class CmsLib
|
||||
$content = $content->saveHTML();
|
||||
$content = str_replace('dms.php', APP_ROOT . 'cms/dms.php', $content);
|
||||
|
||||
return success($content);
|
||||
return success([
|
||||
"content" => $content,
|
||||
"full_count" => $news[0]->full_count ?? 0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +63,10 @@ class News_model extends DB_Model
|
||||
* TODO(chris): this is not a good function -> the params are all over the place
|
||||
*
|
||||
*/
|
||||
protected function prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true)
|
||||
protected function prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true, $filterForDegreePrograms = false, $allowedDegreePrograms = [])
|
||||
{
|
||||
|
||||
$this->addSelect(["*", "COUNT(*) OVER() AS full_count"]);
|
||||
$this->addOrder('datum', 'DESC');
|
||||
|
||||
$studiengang_kz = trim($studiengang_kz);
|
||||
@@ -99,44 +100,45 @@ class News_model extends DB_Model
|
||||
$where[] = "semester = 0";
|
||||
} elseif ($studiengang_kz != '') {
|
||||
$add = $mischen === true ? " OR (studiengang_kz = 0 AND semester IS NULL)" : "";
|
||||
$where[] = "((studiengang_kz = " . $this->db->escape($studiengang_kz) . " AND semester = " . $this->db->escape($semester) . ") OR (studiengang_kz = " . $this->db->escape($studiengang_kz) . " AND semester = 0) OR (studiengang_kz = 0 AND semester = " . $this->db->escape($semester) . ")" . $add . ")";
|
||||
$allowedSemesterValues = [$semester, 0, null];
|
||||
$where[] = "(studiengang_kz = " . $this->db->escape($studiengang_kz) . " AND (semester in (" . implode(',', array_map([$this->db, 'escape'], $allowedSemesterValues)) . ") OR semester IS NULL) OR (studiengang_kz = 0 AND semester = " . $this->db->escape($semester) . ")" . $add . ")";
|
||||
|
||||
}
|
||||
$this->addJoin('campus.tbl_contentsprache cs', 'content_id');
|
||||
|
||||
$where[] = "cs.sichtbar = " . ($sichtbar ? "true" : "false");
|
||||
if (is_bool($sichtbar) || $sichtbar === 'true' || $sichtbar === 'false') {
|
||||
$where[] = "cs.sichtbar = " . ($sichtbar ? "true" : "false");
|
||||
}
|
||||
|
||||
$where[] = "cs.sprache = (CASE WHEN EXISTS(SELECT 1 FROM campus.tbl_contentsprache cs2 WHERE cs2.content_id=" . $this->dbTable . ".content_id AND sprache=" . $this->db->escape($sprache) . ") THEN " . $this->db->escape($sprache) . " ELSE " . $this->db->escape(DEFAULT_LANGUAGE) . " END)";
|
||||
|
||||
|
||||
$where[] = "cs.version = (SELECT MAX(version) FROM campus.tbl_contentsprache cs3 WHERE cs3.content_id=" . $this->dbTable . ".content_id AND cs3.sprache = (CASE WHEN EXISTS(SELECT 1 FROM campus.tbl_contentsprache cs2 WHERE cs2.content_id=" . $this->dbTable . ".content_id AND sprache=" . $this->db->escape($sprache) . ") THEN " . $this->db->escape($sprache) . " ELSE " . $this->db->escape(DEFAULT_LANGUAGE) . " END))";
|
||||
|
||||
|
||||
if ($filterForDegreePrograms) {
|
||||
if (empty($allowedDegreePrograms)) {
|
||||
$where[] = "1=0";
|
||||
} else {
|
||||
$where[] = "studiengang_kz IN (" . implode(',', array_map([$this->db, 'escape'], $allowedDegreePrograms)) . ")";
|
||||
}
|
||||
}
|
||||
|
||||
$where = implode(" AND ", $where);
|
||||
|
||||
$this->db->where($where, NULL, FALSE);
|
||||
|
||||
}
|
||||
|
||||
public function getNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true)
|
||||
public function getNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true, $filterForDegreePrograms = false, $allowedDegreePrograms = [])
|
||||
{
|
||||
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen);
|
||||
|
||||
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
|
||||
// getting the number of rows of the query and adding pagination to the query result
|
||||
$num_rows = $this->getNumRows(true);
|
||||
$this->addPagination($page, $page_size, $num_rows);
|
||||
|
||||
// preparing the query again because every call to get_compiled_select or cour_all_results will add the from clause to the query
|
||||
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen);
|
||||
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen, $filterForDegreePrograms, $allowedDegreePrograms);
|
||||
|
||||
return $this->load();
|
||||
}
|
||||
|
||||
public function countNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz = null, $sichtbar = true, $maxalter = 0, $page = 1, $page_size = 10, $all = false, $mischen = true)
|
||||
{
|
||||
$this->prepareNewsWithContent($sprache, $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $page_size, $all, $mischen);
|
||||
return $this->getNumRows();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -885,6 +885,14 @@ class Studiengang_model extends DB_Model
|
||||
return success($ass);
|
||||
}
|
||||
|
||||
public function getDegreePrograms() {
|
||||
$query = "SELECT *
|
||||
FROM public.tbl_studiengang
|
||||
ORDER BY public.tbl_studiengang.kurzbzlang";
|
||||
|
||||
return $this->execReadOnlyQuery($query);
|
||||
}
|
||||
|
||||
private function addEmailProperty(&$benutzerfunktionen) {
|
||||
if(count($benutzerfunktionen) && defined('DOMAIN'))
|
||||
{
|
||||
|
||||
@@ -24,7 +24,8 @@ $includesArray = array(
|
||||
'public/css/components/FormUnderline.css',
|
||||
'public/css/components/abgabetool/abgabe.css',
|
||||
'public/css/Cis4/Cms.css',
|
||||
'public/css/Cis4/Studium.css'
|
||||
'public/css/Cis4/Studium.css',
|
||||
'public/css/components/vue-datepicker.css',
|
||||
),
|
||||
'customJSs' => array(
|
||||
'vendor/npm-asset/primevue/accordion/accordion.min.js',
|
||||
|
||||
@@ -86,11 +86,11 @@ echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
<link rel="stylesheet" href="../skin/styles/jquery.css" type="text/css">
|
||||
<link rel="stylesheet" href="../vendor/components/jqueryui/themes/base/jquery-ui.min.css" type="text/css">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="../skin/jquery-ui-1.9.2.custom.min.css">
|
||||
<script type="text/javascript" src="../vendor/jquery/jquery1/jquery-1.12.4.min.js"></script>
|
||||
<script type="text/javascript" src="../vendor/christianbach/tablesorter/jquery.tablesorter.min.js"></script>
|
||||
<script type="text/javascript" src="../vendor/components/jqueryui/jquery-ui.min.js"></script>
|
||||
<script type="text/javascript" src="../include/js/jquery.ui.datepicker.translation.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="../skin/jquery-ui-1.9.2.custom.min.css">
|
||||
<script type="text/javascript" src="../vendor/jquery/jquery1/jquery-1.12.4.min.js"></script>
|
||||
<script type="text/javascript" src="../vendor/christianbach/tablesorter/jquery.tablesorter.min.js"></script>
|
||||
<script type="text/javascript" src="../vendor/components/jqueryui/jquery-ui.min.js"></script>
|
||||
<script type="text/javascript" src="../include/js/jquery.ui.datepicker.translation.js"></script>
|
||||
<script type="text/javascript" src="../vendor/jquery/sizzle/sizzle.js"></script>
|
||||
<script src="../vendor/components/jqueryui/jquery-ui.min.js" type="text/javascript"></script>
|
||||
<script type="text/javascript" src="../include/tiny_mce/tiny_mce.js"></script>
|
||||
|
||||
@@ -88,6 +88,29 @@ if (! $rechte->isberechtigt('basis/dms', null, 's', null))
|
||||
init: function(){
|
||||
},
|
||||
mySubmit : function (id) {
|
||||
var messageTarget = window.opener;
|
||||
|
||||
if (!messageTarget && window.parent !== window)
|
||||
messageTarget = window.parent;
|
||||
|
||||
if (messageTarget && typeof(messageTarget.postMessage) == "function")
|
||||
{
|
||||
messageTarget.postMessage(
|
||||
{
|
||||
type: "fhcomplete:dms:selected",
|
||||
dmsId: id
|
||||
},
|
||||
window.location.protocol + "//" + window.location.host
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof(tinyMCEPopup) == "undefined" ||
|
||||
!tinyMCEPopup.params ||
|
||||
typeof(tinyMCEPopup.getWindowArg) != "function"
|
||||
)
|
||||
return;
|
||||
|
||||
var URL = "dms.php?id="+id;
|
||||
var win = tinyMCEPopup.getWindowArg("window");
|
||||
|
||||
|
||||
+17
-38
@@ -1,45 +1,24 @@
|
||||
export default {
|
||||
content(content_id, version=null, sprache=null, sichtbar=null) {
|
||||
return this.$fhcApi.get(
|
||||
"/api/frontend/v1/Cms/content",
|
||||
{
|
||||
content_id: content_id,
|
||||
...(version?{version}:{}),
|
||||
...(sprache?{sprache}:{}),
|
||||
...(sichtbar?{sichtbar}:{}),
|
||||
}
|
||||
);
|
||||
},
|
||||
content(content_id, version = null, sprache = null, sichtbar = null) {
|
||||
return this.$fhcApi.get('/api/frontend/v1/Cms/content', {
|
||||
content_id: content_id,
|
||||
...(version ? { version } : {}),
|
||||
...(sprache ? { sprache } : {}),
|
||||
...(sichtbar ? { sichtbar } : {}),
|
||||
});
|
||||
},
|
||||
//api function used for the news View that renders the html
|
||||
getNews(page = 1, page_size = 10, sprache) {
|
||||
return this.$fhcApi.get(
|
||||
"/api/frontend/v1/Cms/getNews",
|
||||
{
|
||||
page,
|
||||
page_size,
|
||||
sprache,
|
||||
},
|
||||
);
|
||||
return this.$fhcApi.get('/api/frontend/v1/Cms/getNews', {
|
||||
page,
|
||||
page_size,
|
||||
sprache,
|
||||
});
|
||||
},
|
||||
//api function used for the widget component
|
||||
news(limit) {
|
||||
return this.$fhcApi.get(
|
||||
"/api/frontend/v1/Cms/news",
|
||||
{
|
||||
limit: limit
|
||||
}
|
||||
);
|
||||
return this.$fhcApi.get('/api/frontend/v1/Cms/news', {
|
||||
limit: limit,
|
||||
});
|
||||
},
|
||||
getNewsRowCount: function () {
|
||||
return this.$fhcApi.get(
|
||||
"/api/frontend/v1/Cms/getNewsRowCount",
|
||||
{}
|
||||
);
|
||||
},
|
||||
getNewsExtra: function(){
|
||||
return this.$fhcApi.get(
|
||||
"/api/frontend/v1/Cms/getStudiengangInfoForNews",
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
export default {
|
||||
content(content_id, version=null, sprache=null, sichtbar=null) {
|
||||
content(content_id, version = null, sprache = null, sichtbar = null) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Cms/content',
|
||||
@@ -24,12 +24,18 @@ export default {
|
||||
content_id,
|
||||
...(version ? { version } : {}),
|
||||
...(sprache ? { sprache } : {}),
|
||||
...(sichtbar ? { sichtbar } : {})
|
||||
}
|
||||
...(sichtbar ? { sichtbar } : {}),
|
||||
},
|
||||
};
|
||||
},
|
||||
//api function used for the news View that renders the html
|
||||
getNews(page = 1, page_size = 10, sprache, maxAge = 60) {
|
||||
getNews(
|
||||
page = 1,
|
||||
page_size = 10,
|
||||
sprache,
|
||||
maxAge = 60,
|
||||
filterPublished = true,
|
||||
) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Cms/getNews',
|
||||
@@ -37,32 +43,17 @@ export default {
|
||||
page,
|
||||
page_size,
|
||||
sprache,
|
||||
maxAlter: maxAge
|
||||
maxAlter: maxAge,
|
||||
published: filterPublished,
|
||||
},
|
||||
};
|
||||
},
|
||||
//api function used for the widget component
|
||||
news(limit, maxAge = 60) {
|
||||
newsForWidget(limit, maxAge = 60) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Cms/news',
|
||||
params: { limit, maxAlter: maxAge }
|
||||
url: '/api/frontend/v1/Cms/newsForWidget',
|
||||
params: { limit, maxAlter: maxAge },
|
||||
};
|
||||
},
|
||||
getNewsRowCount(maxAge = 60) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Cms/getNewsRowCount',
|
||||
params: {
|
||||
maxAlter: maxAge
|
||||
}
|
||||
};
|
||||
},
|
||||
getNewsExtra() {
|
||||
// TODO(chris): seems to be called from nowhere?
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Cms/getStudiengangInfoForNews'
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (C) 2025 fhcomplete.org
|
||||
*
|
||||
* 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export default {
|
||||
getNews(
|
||||
page = 1,
|
||||
page_size = 10,
|
||||
sprache,
|
||||
maxAge = 60,
|
||||
filterPublished = true,
|
||||
) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/NewsAdministrationAPI/getNews',
|
||||
params: {
|
||||
page,
|
||||
page_size,
|
||||
sprache,
|
||||
maxAlter: maxAge,
|
||||
published: filterPublished,
|
||||
},
|
||||
};
|
||||
},
|
||||
getNewsItem(newsId) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: `/api/frontend/v1/NewsAdministrationAPI/getNewsItem/${newsId}`,
|
||||
};
|
||||
},
|
||||
storeNewsItem(data) {
|
||||
console.log('Storing news item:', data);
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/NewsAdministrationAPI/storeNewsItem',
|
||||
params: data,
|
||||
};
|
||||
},
|
||||
updateNewsItem(newsId, data) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: `/api/frontend/v1/NewsAdministrationAPI/updateNewsItem/${newsId}`,
|
||||
params: data,
|
||||
};
|
||||
},
|
||||
deleteNewsItem(newsId) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: `/api/frontend/v1/NewsAdministrationAPI/deleteNewsItem/${newsId}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -16,17 +16,23 @@
|
||||
*/
|
||||
|
||||
export default {
|
||||
getDegreePrograms() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Studgang/getDegreePrograms',
|
||||
};
|
||||
},
|
||||
studiengangInformation() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/Studgang/getStudiengangInfo'
|
||||
url: '/api/frontend/v1/Studgang/getStudiengangInfo',
|
||||
};
|
||||
},
|
||||
getStudiengangByKz(studiengang_kz) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/organisation/StudiengangEP/getStudiengangByKz',
|
||||
params: { studiengang_kz }
|
||||
params: { studiengang_kz },
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
+133
-111
@@ -2,30 +2,35 @@ import FhcDashboard from '../../components/Dashboard/Dashboard.js';
|
||||
import PluginsPhrasen from '../../plugins/Phrasen.js';
|
||||
import Theme from '../../plugins/Theme.js';
|
||||
import contrast from '../../directives/contrast.js';
|
||||
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
|
||||
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
|
||||
import MyLvPlan from "../../components/Cis/LvPlan/MyLvPlan.js";
|
||||
import MylvStudent from "../../components/Cis/Mylv/Student.js";
|
||||
import Profil from "../../components/Cis/Profil/Profil.js";
|
||||
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js";
|
||||
import CmsNews from "../../components/Cis/Cms/News.js";
|
||||
import NewsItemForm from "../../components/Cis/Cms/NewsItemForm.js";
|
||||
import CmsContent from "../../components/Cis/Cms/Content.js";
|
||||
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
|
||||
import RoomInformation, {DEFAULT_MODE_RAUMINFO_DESKTOP, DEFAULT_MODE_RAUMINFO_MOBILE} from "../../components/Cis/Mylv/RoomInformation.js";
|
||||
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
|
||||
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
|
||||
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
|
||||
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
|
||||
import Studium from "../../components/Cis/Studium/Studium.js";
|
||||
import StgOrgLvPlan from "../../components/Cis/LvPlan/StgOrg.js";
|
||||
import OtherLvPlan from "../../components/Cis/LvPlan/OtherLvPlan.js";
|
||||
import { setScrollbarWidth } from '../../helpers/CssVarCalcHelpers.js';
|
||||
import LvPlan from '../../components/Cis/LvPlan/Lehrveranstaltung.js';
|
||||
import MyLvPlan from '../../components/Cis/LvPlan/MyLvPlan.js';
|
||||
import MylvStudent from '../../components/Cis/Mylv/Student.js';
|
||||
import Profil from '../../components/Cis/Profil/Profil.js';
|
||||
import Raumsuche from '../../components/Cis/Raumsuche/Raumsuche.js';
|
||||
import CmsNews from '../../components/Cis/Cms/News.js';
|
||||
import NewsAdministration from '../../components/Cis/Cms/NewsAdministration/NewsAdministration.js';
|
||||
import CmsContent from '../../components/Cis/Cms/Content.js';
|
||||
import Info from '../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js';
|
||||
import RoomInformation, {
|
||||
DEFAULT_MODE_RAUMINFO_DESKTOP,
|
||||
DEFAULT_MODE_RAUMINFO_MOBILE,
|
||||
} from '../../components/Cis/Mylv/RoomInformation.js';
|
||||
import AbgabetoolStudent from '../../components/Cis/Abgabetool/AbgabetoolStudent.js';
|
||||
import AbgabetoolMitarbeiter from '../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js';
|
||||
import AbgabetoolAssistenz from '../../components/Cis/Abgabetool/AbgabetoolAssistenz.js';
|
||||
import DeadlineOverview from '../../components/Cis/Abgabetool/DeadlineOverview.js';
|
||||
import Studium from '../../components/Cis/Studium/Studium.js';
|
||||
import StgOrgLvPlan from '../../components/Cis/LvPlan/StgOrg.js';
|
||||
import OtherLvPlan from '../../components/Cis/LvPlan/OtherLvPlan.js';
|
||||
|
||||
import ApiRouteInfo from '../../api/factory/routeinfo.js';
|
||||
import {capitalize} from "../../helpers/StringHelpers.js";
|
||||
import { capitalize } from '../../helpers/StringHelpers.js';
|
||||
|
||||
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
|
||||
const isMobile = window.matchMedia("(max-width: 767px)").matches;
|
||||
const ciPath =
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') +
|
||||
FHC_JS_DATA_STORAGE_OBJECT.ci_router;
|
||||
const isMobile = window.matchMedia('(max-width: 767px)').matches;
|
||||
|
||||
const router = VueRouter.createRouter({
|
||||
history: VueRouter.createWebHistory(`/${ciPath}`),
|
||||
@@ -34,62 +39,66 @@ const router = VueRouter.createRouter({
|
||||
path: `/Cis/Studium`,
|
||||
name: 'Studium',
|
||||
component: Studium,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/Cis/Profil/View/:uid`,
|
||||
name: 'ProfilView',
|
||||
component: Profil,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/Cis/Profil`,
|
||||
name: 'Profil',
|
||||
component: Profil,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/Cis/Abgabetool/Student/:student_uid_prop?`,
|
||||
name: 'AbgabetoolStudent',
|
||||
component: AbgabetoolStudent,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/Cis/Abgabetool/Mitarbeiter`,
|
||||
name: 'AbgabetoolMitarbeiter',
|
||||
component: AbgabetoolMitarbeiter,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/Cis/Abgabetool/Assistenz/:stg_kz_prop?`,
|
||||
name: 'AbgabetoolAssistenz',
|
||||
component: AbgabetoolAssistenz,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
|
||||
name: 'DeadlineOverview',
|
||||
component: DeadlineOverview,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/Cis/Raumsuche`,
|
||||
name: 'Raumsuche',
|
||||
component: Raumsuche,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
// Redirect old links to new format
|
||||
{
|
||||
path: "/CisVue/Cms/getRoomInformation/:ort_kurzbz",
|
||||
name: "RoomInformationOld",
|
||||
path: '/CisVue/Cms/getRoomInformation/:ort_kurzbz',
|
||||
name: 'RoomInformationOld',
|
||||
component: RoomInformation,
|
||||
redirect: (to) => {
|
||||
return { // redirect to longer Rauminfo url and map params
|
||||
name: "RoomInformation",
|
||||
params: { // in this case always populate other params since they are not optional
|
||||
return {
|
||||
// redirect to longer Rauminfo url and map params
|
||||
name: 'RoomInformation',
|
||||
params: {
|
||||
// in this case always populate other params since they are not optional
|
||||
ort_kurzbz: to.params.ort_kurzbz,
|
||||
mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP,
|
||||
focus_date: new Date().toISOString().split("T")[0]
|
||||
mode: isMobile
|
||||
? DEFAULT_MODE_RAUMINFO_MOBILE
|
||||
: DEFAULT_MODE_RAUMINFO_DESKTOP,
|
||||
focus_date: new Date().toISOString().split('T')[0],
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -98,18 +107,28 @@ const router = VueRouter.createRouter({
|
||||
path: `/CisVue/Cms/getRoomInformation/:mode/:focus_date/:ort_kurzbz`,
|
||||
name: 'RoomInformation',
|
||||
component: RoomInformation,
|
||||
props: (route) => { // validate and set mode/focus date if for some reason missing
|
||||
const validModes = ["Month", "Week", "Day"];
|
||||
props: (route) => {
|
||||
// validate and set mode/focus date if for some reason missing
|
||||
const validModes = ['Month', 'Week', 'Day'];
|
||||
|
||||
// check mode string
|
||||
const mode = route.params.mode &&
|
||||
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase())
|
||||
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
|
||||
: (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP);
|
||||
const mode =
|
||||
route.params.mode &&
|
||||
validModes.includes(
|
||||
route.params.mode.charAt(0).toUpperCase() +
|
||||
route.params.mode.slice(1).toLowerCase(),
|
||||
)
|
||||
? route.params.mode.charAt(0).toUpperCase() +
|
||||
route.params.mode.slice(1).toLowerCase()
|
||||
: isMobile
|
||||
? DEFAULT_MODE_RAUMINFO_MOBILE
|
||||
: DEFAULT_MODE_RAUMINFO_DESKTOP;
|
||||
|
||||
// default to today date if not provided
|
||||
const d = new Date(route.params.focus_date)
|
||||
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
|
||||
const d = new Date(route.params.focus_date);
|
||||
const focus_date = !isNaN(d)
|
||||
? route.params.focus_date
|
||||
: new Date().toISOString().split('T')[0];
|
||||
|
||||
// for consistency reasons format the props into one object but actually use a new name to we dont collide with
|
||||
// existing viewData declaration written from codeigniter 3 into routerview tag
|
||||
@@ -117,43 +136,44 @@ const router = VueRouter.createRouter({
|
||||
propsViewData: {
|
||||
mode,
|
||||
focus_date,
|
||||
ort_kurzbz: route.params.ort_kurzbz
|
||||
}
|
||||
ort_kurzbz: route.params.ort_kurzbz,
|
||||
},
|
||||
};
|
||||
},
|
||||
beforeEnter: (to, from, next) => {
|
||||
// missing mode or focus_date -> set defaults
|
||||
if (!to.params.mode || !to.params.focus_date) {
|
||||
next({
|
||||
name: "RoomInformation",
|
||||
name: 'RoomInformation',
|
||||
params: {
|
||||
mode: to.params.mode || DEFAULT_MODE_RAUMINFO,
|
||||
focus_date: to.params.focus_date || new Date().toISOString().split("T")[0],
|
||||
ort_kurzbz: route.params.ort_kurzbz
|
||||
}
|
||||
focus_date:
|
||||
to.params.focus_date || new Date().toISOString().split('T')[0],
|
||||
ort_kurzbz: route.params.ort_kurzbz,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/CisVue/Cms/Content/:content_id`,
|
||||
name: 'Content',
|
||||
component: CmsContent,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/CisVue/Cms/News`,
|
||||
name: 'News',
|
||||
component: CmsNews,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/CisVue/Cms/News/Create`,
|
||||
name: 'NewsItemCreate',
|
||||
component: NewsItemForm,
|
||||
props: true
|
||||
path: `/CisVue/Cms/NewsAdministration`,
|
||||
name: 'NewsAdministration',
|
||||
component: NewsAdministration,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: `/Cis/MyLv/:studiensemester?`,
|
||||
@@ -165,23 +185,24 @@ const router = VueRouter.createRouter({
|
||||
path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`,
|
||||
name: 'LvInfo',
|
||||
component: Info,
|
||||
props: true
|
||||
props: true,
|
||||
},
|
||||
// Redirect old links to new format
|
||||
{
|
||||
// only trigger on first param being numeric to avoid paths like "LvPlan/Month" entering here
|
||||
path: "/Cis/LvPlan/:lv_id(\\d+)",
|
||||
name: "LvPlanOld",
|
||||
path: '/Cis/LvPlan/:lv_id(\\d+)',
|
||||
name: 'LvPlanOld',
|
||||
component: LvPlan,
|
||||
redirect(to) {
|
||||
const route = Vue.unref(router.currentRoute);
|
||||
const { mode, focus_date } = route.params; // keep mode and focus_date if available
|
||||
return { // redirect to longer LvPlan url and map params
|
||||
name: "LvPlan",
|
||||
return {
|
||||
// redirect to longer LvPlan url and map params
|
||||
name: 'LvPlan',
|
||||
params: {
|
||||
mode,
|
||||
focus_date,
|
||||
lv_id: to.params.lv_id
|
||||
lv_id: to.params.lv_id,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -192,9 +213,9 @@ const router = VueRouter.createRouter({
|
||||
component: LvPlan,
|
||||
props(route) {
|
||||
return {
|
||||
propsViewData: route.params
|
||||
propsViewData: route.params,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
|
||||
@@ -202,9 +223,9 @@ const router = VueRouter.createRouter({
|
||||
component: MyLvPlan,
|
||||
props(route) {
|
||||
return {
|
||||
propsViewData: route.params
|
||||
propsViewData: route.params,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/Cis/StgOrgLvPlan/:mode?/:focus_date?/:stgkz?/:sem?/:verband?/:gruppe?`,
|
||||
@@ -212,48 +233,48 @@ const router = VueRouter.createRouter({
|
||||
component: StgOrgLvPlan,
|
||||
props(route) {
|
||||
return {
|
||||
propsViewData: route.params
|
||||
propsViewData: route.params,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`,
|
||||
name: "OtherLvPlan",
|
||||
component: OtherLvPlan,
|
||||
props(route) {
|
||||
return {
|
||||
propsViewData: route.params
|
||||
};
|
||||
}
|
||||
},
|
||||
path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`,
|
||||
name: 'OtherLvPlan',
|
||||
component: OtherLvPlan,
|
||||
props(route) {
|
||||
return {
|
||||
propsViewData: route.params,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `/Cis4`,
|
||||
name: 'Cis4',
|
||||
component: FhcDashboard,
|
||||
props: {dashboard: 'CIS'},
|
||||
props: { dashboard: 'CIS' },
|
||||
},
|
||||
{
|
||||
path: `/`,
|
||||
name: 'FhcDashboard',
|
||||
component: FhcDashboard,
|
||||
props: {dashboard: 'CIS'},
|
||||
props: { dashboard: 'CIS' },
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'Fallback',
|
||||
component: FhcDashboard,
|
||||
props: {dashboard: 'CIS'},
|
||||
props: { dashboard: 'CIS' },
|
||||
redirect: () => {
|
||||
return {
|
||||
name: "Cis4",
|
||||
name: 'Cis4',
|
||||
params: {
|
||||
dashboard: 'CIS'
|
||||
dashboard: 'CIS',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
],
|
||||
});
|
||||
|
||||
const app = Vue.createApp({
|
||||
name: 'CisApp',
|
||||
@@ -262,48 +283,49 @@ const app = Vue.createApp({
|
||||
windowWidth: 0,
|
||||
}),
|
||||
provide() {
|
||||
return { // provide injectable & watchable language property
|
||||
return {
|
||||
// provide injectable & watchable language property
|
||||
language: Vue.computed(() => this.$p.user_language),
|
||||
isMobile: Vue.computed(() => this.isMobile),
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMobile: function() {
|
||||
return (this.windowWidth < 767);
|
||||
}
|
||||
isMobile: function () {
|
||||
return this.windowWidth < 767;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isInternalRoute(href) {
|
||||
const internalBase = window.location.origin
|
||||
const internalBase = window.location.origin;
|
||||
return href.startsWith(internalBase);
|
||||
},
|
||||
handleClick(event) {
|
||||
const target = event.target.closest('a');
|
||||
|
||||
if(target?.id == 'skiplink') return
|
||||
if (target?.id == 'skiplink') return;
|
||||
if (target && this.isInternalRoute(target.href)) {
|
||||
const url = new URL(target.href)
|
||||
|
||||
const path = url.pathname
|
||||
const base = this.$router.options.history.base
|
||||
const route = path.replace(base, '') || '/'
|
||||
const url = new URL(target.href);
|
||||
|
||||
const path = url.pathname;
|
||||
const base = this.$router.options.history.base;
|
||||
const route = path.replace(base, '') || '/';
|
||||
|
||||
// let click event propagate normally if we dont route internally
|
||||
const res = this.$router.resolve(route)
|
||||
if(!res?.matched?.length || res.name === 'Fallback') return
|
||||
|
||||
const res = this.$router.resolve(route);
|
||||
if (!res?.matched?.length || res.name === 'Fallback') return;
|
||||
|
||||
event.preventDefault(); // Prevent browser navigation
|
||||
|
||||
if(this.isMobile) { // toggle the menu
|
||||
|
||||
if (this.isMobile) {
|
||||
// toggle the menu
|
||||
const navMain = document.getElementById('nav-main');
|
||||
// fix unwanted toggle from off to on for some links on mobile
|
||||
if(navMain.classList.contains('show')){
|
||||
if (navMain.classList.contains('show')) {
|
||||
document.getElementById('nav-main-btn').click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.$router.push(route);
|
||||
|
||||
}
|
||||
},
|
||||
handleWindowResize() {
|
||||
@@ -315,11 +337,11 @@ const app = Vue.createApp({
|
||||
},
|
||||
async mounted() {
|
||||
document.addEventListener('click', this.handleClick);
|
||||
window.addEventListener("resize", this.handleWindowResize);
|
||||
window.addEventListener('resize', this.handleWindowResize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('click', this.handleClick);
|
||||
window.removeEventListener("resize", this.handleWindowResize);
|
||||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -334,9 +356,9 @@ app.use(router);
|
||||
app.use(primevue.config.default, {
|
||||
zIndex: {
|
||||
overlay: 9000,
|
||||
tooltip: 8000
|
||||
}
|
||||
})
|
||||
tooltip: 8000,
|
||||
},
|
||||
});
|
||||
app.directive('tooltip', primevue.tooltip);
|
||||
app.use(PluginsPhrasen);
|
||||
app.use(Theme);
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import Pagination from "../../Pagination/Pagination.js";
|
||||
import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js";
|
||||
import BsConfirm from "../../Bootstrap/Confirm.js";
|
||||
import Pagination from '../../Pagination/Pagination.js';
|
||||
import StudiengangInformation from './StudiengangInformation/StudiengangInformation.js';
|
||||
import BsConfirm from '../../Bootstrap/Confirm.js';
|
||||
|
||||
import ApiCms from "../../../api/factory/cms.js";
|
||||
import ApiCms from '../../../api/factory/cms.js';
|
||||
|
||||
const MAX_NEW_AGE = 60;
|
||||
|
||||
export default {
|
||||
name: "NewsComponent",
|
||||
name: 'NewsComponent',
|
||||
components: {
|
||||
Pagination,
|
||||
StudiengangInformation,
|
||||
},
|
||||
inject: ["isMobile"],
|
||||
inject: ['isMobile'],
|
||||
data() {
|
||||
return {
|
||||
content: null,
|
||||
maxPageCount: 0,
|
||||
page_size: 10,
|
||||
page_size: 100,
|
||||
page: 1,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
"$p.user_language.value": function (sprache) {
|
||||
'$p.user_language.value': function (sprache) {
|
||||
this.fetchNews();
|
||||
},
|
||||
},
|
||||
@@ -37,9 +37,10 @@ export default {
|
||||
ApiCms.getNews(this.page, this.page_size, this.sprache, MAX_NEW_AGE),
|
||||
);
|
||||
this.content = newsResponse.data;
|
||||
this.maxPageCount = newsResponse.meta.row_count;
|
||||
|
||||
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
|
||||
el.addEventListener("click", (evt) => {
|
||||
document.querySelectorAll('#cms [data-confirm]').forEach((el) => {
|
||||
el.addEventListener('click', (evt) => {
|
||||
evt.preventDefault();
|
||||
BsConfirm.popup(el.dataset.confirm)
|
||||
.then(() => {
|
||||
@@ -48,12 +49,12 @@ export default {
|
||||
// TODO(chris): check for success then show message and/or reload
|
||||
location = location;
|
||||
})
|
||||
.catch((err) => console.error("ERROR:", err));
|
||||
.catch((err) => console.error('ERROR:', err));
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#cms [data-href]").forEach((el) => {
|
||||
document.querySelectorAll('#cms [data-href]').forEach((el) => {
|
||||
el.href = el.dataset.href.replace(
|
||||
/^ROOT\//,
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root,
|
||||
@@ -75,36 +76,27 @@ export default {
|
||||
},
|
||||
formatExternalHtml() {
|
||||
document
|
||||
.querySelectorAll(".news-list-item .card-header")
|
||||
.querySelectorAll('.news-list-item .card-header')
|
||||
.forEach((el) => {
|
||||
el.classList.add("fhc-primary");
|
||||
el.classList.add('fhc-primary');
|
||||
});
|
||||
document.querySelectorAll(".news-list-item .row").forEach((el) => {
|
||||
el.classList.add("w-100");
|
||||
el.classList.add("align-items-center");
|
||||
document.querySelectorAll('.news-list-item .row').forEach((el) => {
|
||||
el.classList.add('w-100');
|
||||
el.classList.add('align-items-center');
|
||||
});
|
||||
document.querySelectorAll('.news-list-item .row h2').forEach((el) => {
|
||||
el.classList.add('mb-0');
|
||||
});
|
||||
document
|
||||
.querySelectorAll(".news-list-item .row h2")
|
||||
.forEach((el) => {
|
||||
el.classList.add("mb-0");
|
||||
});
|
||||
},
|
||||
afterPageUpdated(event) {
|
||||
this.page = event.page;
|
||||
this.page_size = event.rows;
|
||||
this.$refs.newsPageHeading.scrollIntoView({block: 'end'});
|
||||
this.$refs.newsPageHeading.scrollIntoView({ block: 'end' });
|
||||
this.loadNewPageContent(event);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchNews();
|
||||
|
||||
this.$api
|
||||
.call(ApiCms.getNewsRowCount(MAX_NEW_AGE))
|
||||
.then((res) => res.data)
|
||||
.then((result) => {
|
||||
this.maxPageCount = result;
|
||||
});
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import NewsItemForm from './NewsItemForm.js';
|
||||
import NewsItemPreview from './NewsItemPreview.js';
|
||||
import NewsList from './NewsList.js';
|
||||
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
|
||||
|
||||
export default {
|
||||
name: 'NewsAdministration',
|
||||
components: {
|
||||
NewsItemForm,
|
||||
NewsItemPreview,
|
||||
NewsList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isNewsFormShown: false,
|
||||
isNewsPreviewShown: false,
|
||||
newsToEdit: null,
|
||||
newsPreview: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async reloadNews() {
|
||||
await this.$refs.newsList.reload();
|
||||
},
|
||||
async handleNewsItemUpdated() {
|
||||
this.isNewsFormShown = false;
|
||||
this.isNewsPreviewShown = false;
|
||||
this.newsToEdit = null;
|
||||
this.newsPreview = null;
|
||||
await this.reloadNews();
|
||||
},
|
||||
handleNewsItemSaved() {
|
||||
this.reloadNews();
|
||||
this.isNewsFormShown = false;
|
||||
this.isNewsPreviewShown = false;
|
||||
this.newsToEdit = null;
|
||||
this.newsPreview = null;
|
||||
},
|
||||
showCreateNewsForm() {
|
||||
this.newsToEdit = null;
|
||||
this.isNewsPreviewShown = false;
|
||||
this.isNewsFormShown = true;
|
||||
},
|
||||
async editNews(news) {
|
||||
console.log('Editing news item:', news);
|
||||
try {
|
||||
const response = await this.$api.call(
|
||||
ApiNewsAdministration.getNewsItem(news.newsId),
|
||||
);
|
||||
const freshNews = response.data;
|
||||
|
||||
if (!freshNews) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.newsToEdit = freshNews;
|
||||
this.isNewsPreviewShown = false;
|
||||
this.isNewsFormShown = true;
|
||||
} catch (error) {
|
||||
this.$fhcAlert.handleSystemError(error);
|
||||
}
|
||||
},
|
||||
cancelNewsForm() {
|
||||
this.isNewsFormShown = false;
|
||||
this.isNewsPreviewShown = false;
|
||||
this.newsToEdit = null;
|
||||
this.newsPreview = null;
|
||||
},
|
||||
toggleNewsPreview() {
|
||||
this.isNewsPreviewShown = !this.isNewsPreviewShown;
|
||||
},
|
||||
handlePreviewChange(preview) {
|
||||
this.newsPreview = preview;
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 ref="newsPageHeading" class="fhc-primary-color mb-0">News Administration</h2>
|
||||
<button
|
||||
v-if="!isNewsFormShown"
|
||||
@click="showCreateNewsForm"
|
||||
type="button"
|
||||
class="btn btn-primary rounded-circle d-inline-flex align-items-center justify-content-center p-0"
|
||||
style="width: 2.5rem; height: 2.5rem"
|
||||
:title="$p.t('news', 'createNews')"
|
||||
:aria-label="$p.t('news', 'createNews')"
|
||||
aria-controls="news-item-form"
|
||||
>
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<Transition>
|
||||
<div v-if="isNewsFormShown" class="row g-4 align-items-start">
|
||||
<div :class="isNewsPreviewShown ? 'col-12 col-xl-6' : 'col-12'">
|
||||
<news-item-form
|
||||
:key="newsToEdit?.newsId || 'new-news-item'"
|
||||
id="news-item-form"
|
||||
:news="newsToEdit"
|
||||
:is-preview-shown="isNewsPreviewShown"
|
||||
@created="handleNewsItemSaved"
|
||||
@update="handleNewsItemUpdated"
|
||||
@cancel="cancelNewsForm"
|
||||
@preview-change="handlePreviewChange"
|
||||
@toggle-preview="toggleNewsPreview"
|
||||
></news-item-form>
|
||||
</div>
|
||||
<div v-if="isNewsPreviewShown" class="col-12 col-xl-6">
|
||||
<news-item-preview
|
||||
:form-data="newsPreview?.formData"
|
||||
:active-language="newsPreview?.activeLanguage"
|
||||
></news-item-preview>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<news-list
|
||||
ref="newsList"
|
||||
@edit="editNews"
|
||||
></news-list>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import BootstrapModal from '../../../Bootstrap/Modal.js';
|
||||
|
||||
export default {
|
||||
name: 'NewsItemAddLanguageModal',
|
||||
components: {
|
||||
BootstrapModal,
|
||||
},
|
||||
emits: ['add-language', 'hidden'],
|
||||
props: {
|
||||
languages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<bootstrap-modal
|
||||
ref="modal"
|
||||
dialog-class="modal-dialog-centered"
|
||||
body-class="p-4"
|
||||
@hidden-bs-modal="$emit('hidden')"
|
||||
>
|
||||
<template #title>Add translation language</template>
|
||||
<div v-if="languages.length" class="d-grid gap-2">
|
||||
<button
|
||||
v-for="language in languages"
|
||||
:key="language.value"
|
||||
type="button"
|
||||
class="btn btn-outline-primary text-start"
|
||||
@click="$emit('add-language', language.value)"
|
||||
>
|
||||
{{ language.label }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="mb-0 text-muted">All available languages have been added.</p>
|
||||
</bootstrap-modal>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
name: 'NewsItemAddLanguageTab',
|
||||
emits: ['tab-action'],
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="py-4 text-center text-muted">
|
||||
<button
|
||||
v-if="config.hasAvailableLanguages"
|
||||
type="button"
|
||||
class="btn btn-link text-decoration-none"
|
||||
aria-label="Add translation language"
|
||||
@click="$emit('tab-action')"
|
||||
>
|
||||
<i class="fa-solid fa-plus-circle fs-4 mb-2" aria-hidden="true"></i>
|
||||
<span class="d-block">Select a language to add a translation.</span>
|
||||
</button>
|
||||
<p v-else class="mb-0">All available languages have been added.</p>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
import FormInput from '../../../Form/Input.js';
|
||||
|
||||
export default {
|
||||
name: 'NewsItemContentForm',
|
||||
components: {
|
||||
FormInput,
|
||||
},
|
||||
emits: ['update:modelValue', 'tab-action'],
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const translation = this.modelValue.find(
|
||||
(item) => item.language === this.config.language,
|
||||
);
|
||||
|
||||
return {
|
||||
editor: null,
|
||||
dmsFileBrowser: null,
|
||||
dmsFileBrowserClosePollId: null,
|
||||
dmsFileBrowserMessageHandler: null,
|
||||
contentData: {
|
||||
language: this.config.language,
|
||||
author: '',
|
||||
title: '',
|
||||
text: '',
|
||||
isPublished: false,
|
||||
...translation,
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
contentData: {
|
||||
deep: true,
|
||||
handler(contentData) {
|
||||
const translations = this.modelValue.map((item) => ({ ...item }));
|
||||
const index = translations.findIndex(
|
||||
(item) => item.language === this.config.language,
|
||||
);
|
||||
const translation = {
|
||||
...contentData,
|
||||
language: this.config.language,
|
||||
};
|
||||
|
||||
if (index === -1) {
|
||||
translations.push(translation);
|
||||
} else {
|
||||
translations[index] = translation;
|
||||
}
|
||||
|
||||
this.$emit('update:modelValue', translations);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cleanupDmsFileBrowser(closePopup = false) {
|
||||
if (this.dmsFileBrowserClosePollId !== null) {
|
||||
window.clearInterval(this.dmsFileBrowserClosePollId);
|
||||
this.dmsFileBrowserClosePollId = null;
|
||||
}
|
||||
|
||||
if (this.dmsFileBrowserMessageHandler) {
|
||||
window.removeEventListener(
|
||||
'message',
|
||||
this.dmsFileBrowserMessageHandler,
|
||||
);
|
||||
this.dmsFileBrowserMessageHandler = null;
|
||||
}
|
||||
|
||||
if (
|
||||
closePopup &&
|
||||
this.dmsFileBrowser &&
|
||||
!this.dmsFileBrowser.closed
|
||||
) {
|
||||
this.dmsFileBrowser.close();
|
||||
}
|
||||
|
||||
this.dmsFileBrowser = null;
|
||||
},
|
||||
openDmsFileBrowser(callback, meta) {
|
||||
this.cleanupDmsFileBrowser(true);
|
||||
|
||||
const appRoot =
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/\/+$/, '') + '/';
|
||||
const fileType = meta && meta.filetype ? meta.filetype : 'file';
|
||||
const fileBrowserUrl =
|
||||
appRoot +
|
||||
'cms/tinymce_dms.php?type=' +
|
||||
encodeURIComponent(fileType);
|
||||
const popupName = this.editor
|
||||
? 'FHCFileBrowser_' + this.editor.id
|
||||
: 'FHCFileBrowser';
|
||||
const popup = window.open(
|
||||
fileBrowserUrl,
|
||||
popupName,
|
||||
'width=750,height=550,resizable=yes,scrollbars=yes',
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dmsFileBrowser = popup;
|
||||
|
||||
this.dmsFileBrowserMessageHandler = (event) => {
|
||||
const message = event.data;
|
||||
|
||||
if (
|
||||
event.origin !== window.location.origin ||
|
||||
event.source !== popup ||
|
||||
this.dmsFileBrowser !== popup ||
|
||||
!message ||
|
||||
message.type !== 'fhcomplete:dms:selected' ||
|
||||
!Number.isInteger(message.dmsId) ||
|
||||
message.dmsId <= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
callback(
|
||||
appRoot +
|
||||
'cms/dms.php?id=' +
|
||||
encodeURIComponent(message.dmsId),
|
||||
);
|
||||
} finally {
|
||||
this.cleanupDmsFileBrowser(true);
|
||||
}
|
||||
};
|
||||
window.addEventListener(
|
||||
'message',
|
||||
this.dmsFileBrowserMessageHandler,
|
||||
);
|
||||
|
||||
this.dmsFileBrowserClosePollId = window.setInterval(() => {
|
||||
if (this.dmsFileBrowser === popup && popup.closed) {
|
||||
this.cleanupDmsFileBrowser();
|
||||
}
|
||||
}, 250);
|
||||
popup.focus();
|
||||
},
|
||||
initTinyMCE() {
|
||||
const vm = this;
|
||||
tinymce.init({
|
||||
target: this.$refs.editor.$refs.input, // Important: enables multiple component instances
|
||||
toolbar:
|
||||
'styleselect fontsizeselect | bold italic underline | alignleft aligncenter alignright alignjustify | link unlink image | bullist | pastetext',
|
||||
plugins: 'lists link image paste',
|
||||
paste_as_text: true,
|
||||
statusbar: true,
|
||||
resize: true,
|
||||
file_picker_types: 'file image',
|
||||
file_picker_callback: (callback, _value, meta) => {
|
||||
vm.openDmsFileBrowser(callback, meta);
|
||||
},
|
||||
|
||||
style_formats: [
|
||||
{ title: 'Blocks', block: 'div' },
|
||||
{ title: 'Paragraph', block: 'p' },
|
||||
{ title: 'Heading 1', block: 'h1' },
|
||||
{ title: 'Heading 2', block: 'h2' },
|
||||
{ title: 'Heading 3', block: 'h3' },
|
||||
{ title: 'Heading 4', block: 'h4' },
|
||||
{ title: 'Heading 5', block: 'h5' },
|
||||
{ title: 'Heading 6', block: 'h6' },
|
||||
],
|
||||
setup: (editor) => {
|
||||
vm.editor = editor;
|
||||
|
||||
editor.on('input change undo redo', () => {
|
||||
vm.contentData.text = editor.getContent();
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initTinyMCE();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.cleanupDmsFileBrowser(true);
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div :class="{'pb-3': isMobile}" class="position-relative overflow-x-hidden">
|
||||
<button
|
||||
v-if="config.language !== 'German'"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger position-absolute top-0 end-0"
|
||||
title="Remove language"
|
||||
aria-label="Remove language"
|
||||
@click="$emit('tab-action', { action: 'remove-language', language: config.language })"
|
||||
>
|
||||
<i class="fa-solid fa-trash-can" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-end gap-3">
|
||||
<form-input
|
||||
type="text"
|
||||
:label="$p.t('notiz', 'verfasser')"
|
||||
name="author"
|
||||
v-model="contentData.author"
|
||||
>
|
||||
</form-input>
|
||||
<form-input
|
||||
type="text"
|
||||
:label="$p.t('global', 'betreff')"
|
||||
name="title"
|
||||
v-model="contentData.title"
|
||||
>
|
||||
</form-input>
|
||||
<form-input
|
||||
type="checkbox"
|
||||
:label="$p.t('ui', 'publish')"
|
||||
name="isPublished"
|
||||
v-model="contentData.isPublished"
|
||||
>
|
||||
</form-input>
|
||||
</div>
|
||||
<form-input
|
||||
ref="editor"
|
||||
:label="$p.t('global','text') + ' *'"
|
||||
type="textarea"
|
||||
name="text"
|
||||
v-model="contentData.text"
|
||||
rows="5"
|
||||
cols="75"
|
||||
>
|
||||
</form-input>
|
||||
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import BootstrapModal from '../../../Bootstrap/Modal.js';
|
||||
|
||||
export default {
|
||||
name: 'NewsItemCopyTranslationModal',
|
||||
components: {
|
||||
BootstrapModal,
|
||||
},
|
||||
emits: ['copy', 'hidden'],
|
||||
props: {
|
||||
sourceLanguages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
targetLanguageLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
targetHasContent: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedSourceLanguage: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.selectedSourceLanguage = null;
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
selectSourceLanguage(language) {
|
||||
if (this.targetHasContent) {
|
||||
this.selectedSourceLanguage = language;
|
||||
return;
|
||||
}
|
||||
|
||||
this.copyTranslation(language);
|
||||
},
|
||||
copyTranslation(language = this.selectedSourceLanguage) {
|
||||
const sourceLanguage = language?.value ?? language;
|
||||
|
||||
if (!sourceLanguage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('copy', sourceLanguage);
|
||||
this.hide();
|
||||
},
|
||||
resetSelection() {
|
||||
this.selectedSourceLanguage = null;
|
||||
this.$emit('hidden');
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<bootstrap-modal
|
||||
ref="modal"
|
||||
dialog-class="modal-dialog-centered"
|
||||
body-class="p-4"
|
||||
@hidden-bs-modal="resetSelection"
|
||||
>
|
||||
<template #title>
|
||||
{{ selectedSourceLanguage ? 'Overwrite translation content?' : 'Copy translation content' }}
|
||||
</template>
|
||||
<template v-if="selectedSourceLanguage">
|
||||
<p class="mb-3">
|
||||
Copying from <strong>{{ selectedSourceLanguage.label }}</strong> will replace the author, title, and text in <strong>{{ targetLanguageLabel }}</strong>.
|
||||
</p>
|
||||
<p class="text-muted small">The existing content in the current language will be lost.</p>
|
||||
<div class="d-flex justify-content-end gap-2 mt-4">
|
||||
<button type="button" class="btn btn-outline-secondary" @click="selectedSourceLanguage = null">
|
||||
Choose another language
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="copyTranslation()">
|
||||
Overwrite and copy
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="text-muted">Choose the language whose content should be copied into {{ targetLanguageLabel }}.</p>
|
||||
<div v-if="sourceLanguages.length" class="d-grid gap-2">
|
||||
<button
|
||||
v-for="language in sourceLanguages"
|
||||
:key="language.value"
|
||||
type="button"
|
||||
class="btn btn-outline-primary text-start"
|
||||
@click="selectSourceLanguage(language)"
|
||||
>
|
||||
{{ language.label }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="mb-0 text-muted">Add another language before copying content.</p>
|
||||
</template>
|
||||
</bootstrap-modal>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,636 @@
|
||||
import CoreForm from '../../../Form/Form.js';
|
||||
import FormInput from '../../../Form/Input.js';
|
||||
import FhcTabs from '../../../Tabs.js';
|
||||
import NewsItemAddLanguageModal from './NewsItemAddLanguageModal.js';
|
||||
import NewsItemCopyTranslationModal from './NewsItemCopyTranslationModal.js';
|
||||
import ApiStudiengang from '../../../../api/factory/studiengang.js';
|
||||
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
|
||||
|
||||
const BASE_COMPONENT_URL =
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
'public/js/components/Cis/Cms/NewsAdministration/';
|
||||
|
||||
export default {
|
||||
name: 'NewsItemForm',
|
||||
components: {
|
||||
CoreForm,
|
||||
FormInput,
|
||||
FhcTabs,
|
||||
NewsItemAddLanguageModal,
|
||||
NewsItemCopyTranslationModal,
|
||||
},
|
||||
emits: [
|
||||
'created',
|
||||
'update',
|
||||
'cancel',
|
||||
'translation-copied',
|
||||
'preview-change',
|
||||
'toggle-preview',
|
||||
],
|
||||
props: {
|
||||
news: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isPreviewShown: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
degreePrograms: [],
|
||||
filteredDegreePrograms: [],
|
||||
isSaving: false,
|
||||
activeContentFormKey: 'germanContentForm',
|
||||
contentFormRenderKey: 0,
|
||||
availableLanguages: [
|
||||
{ label: 'German', value: 'German' },
|
||||
{ label: 'English', value: 'English' },
|
||||
{ label: 'French', value: 'French' },
|
||||
{ label: 'Spanish', value: 'Spanish' },
|
||||
],
|
||||
semesters: [
|
||||
{
|
||||
label: 'Alle Semester',
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
label: '1. Semester',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: '2. Semester',
|
||||
value: '2',
|
||||
},
|
||||
{
|
||||
label: '3. Semester',
|
||||
value: '3',
|
||||
},
|
||||
{
|
||||
label: '4. Semester',
|
||||
value: '4',
|
||||
},
|
||||
{
|
||||
label: '5. Semester',
|
||||
value: '5',
|
||||
},
|
||||
{
|
||||
label: '6. Semester',
|
||||
value: '6',
|
||||
},
|
||||
{
|
||||
label: '7. Semester',
|
||||
value: '7',
|
||||
},
|
||||
{
|
||||
label: '8. Semester',
|
||||
value: '8',
|
||||
},
|
||||
],
|
||||
formData: {
|
||||
visibleFrom: null,
|
||||
visibleTo: null,
|
||||
degreeProgram: null,
|
||||
semester: null,
|
||||
translations: [
|
||||
{
|
||||
language: 'German',
|
||||
author: '',
|
||||
title: '',
|
||||
text: '',
|
||||
isPublished: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
contentFormItems: {
|
||||
germanContentForm: {
|
||||
title: 'German Content Form',
|
||||
component: BASE_COMPONENT_URL + 'NewsItemContentForm.js',
|
||||
config: {
|
||||
language: 'German',
|
||||
type: 'news',
|
||||
},
|
||||
key: 'germanContentForm',
|
||||
},
|
||||
addLanguage: {
|
||||
title: '+',
|
||||
component: BASE_COMPONENT_URL + 'NewsItemAddLanguageTab.js',
|
||||
config: { hasAvailableLanguages: true },
|
||||
key: 'addLanguage',
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'$p.user_language.value': function (sprache) {
|
||||
this.fetchNews();
|
||||
},
|
||||
formData: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.emitPreviewChange();
|
||||
},
|
||||
},
|
||||
activeContentLanguage() {
|
||||
this.emitPreviewChange();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sprache: function () {
|
||||
return this.$p.user_language.value;
|
||||
},
|
||||
dropdownParsedDegreePrograms() {
|
||||
return this.degreePrograms
|
||||
.map((degreeProgram) => {
|
||||
let labelFragment = degreeProgram.typ + degreeProgram.kurzbz;
|
||||
labelFragment = labelFragment.trim().toUpperCase();
|
||||
|
||||
return {
|
||||
label: `${labelFragment} (${degreeProgram.bezeichnung})`,
|
||||
value: degreeProgram.studiengang_kz,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
},
|
||||
visibleContentLanguages() {
|
||||
return Object.values(this.contentFormItems)
|
||||
.map((item) => item.config?.language)
|
||||
.filter(Boolean);
|
||||
},
|
||||
languagesToAdd() {
|
||||
return this.availableLanguages.filter(
|
||||
(language) => !this.visibleContentLanguages.includes(language.value),
|
||||
);
|
||||
},
|
||||
activeContentLanguage() {
|
||||
return this.contentFormItems[this.activeContentFormKey]?.config?.language;
|
||||
},
|
||||
copySourceLanguages() {
|
||||
return this.visibleContentLanguages
|
||||
.filter((language) => language !== this.activeContentLanguage)
|
||||
.map((language) => ({
|
||||
label: this.getLanguageLabel(language),
|
||||
value: language,
|
||||
}));
|
||||
},
|
||||
activeTranslationHasContent() {
|
||||
return this.hasTranslationContent(
|
||||
this.getTranslation(this.activeContentLanguage),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emitPreviewChange() {
|
||||
this.$emit('preview-change', {
|
||||
formData: this.formData,
|
||||
activeLanguage: this.activeContentLanguage,
|
||||
});
|
||||
},
|
||||
getContentFormKey(language) {
|
||||
return `${language.toLowerCase()}ContentForm`;
|
||||
},
|
||||
createContentFormItem(language) {
|
||||
const key = this.getContentFormKey(language);
|
||||
|
||||
return {
|
||||
title: `${language} Content Form`,
|
||||
component: BASE_COMPONENT_URL + 'NewsItemContentForm.js?124',
|
||||
config: {
|
||||
language,
|
||||
type: 'news',
|
||||
},
|
||||
key,
|
||||
};
|
||||
},
|
||||
showLanguageModal() {
|
||||
this.$refs.addLanguageModal.show();
|
||||
},
|
||||
showCopyTranslationModal() {
|
||||
this.$refs.copyTranslationModal.show();
|
||||
},
|
||||
getLanguageLabel(language) {
|
||||
return (
|
||||
this.availableLanguages.find(
|
||||
(availableLanguage) => availableLanguage.value === language,
|
||||
)?.label ?? language
|
||||
);
|
||||
},
|
||||
getTranslation(language) {
|
||||
return this.formData.translations.find(
|
||||
(translation) => translation.language === language,
|
||||
);
|
||||
},
|
||||
hasTranslationContent(translation) {
|
||||
return ['author', 'title', 'text'].some(
|
||||
(field) => String(translation?.[field] ?? '').trim() !== '',
|
||||
);
|
||||
},
|
||||
copyTranslation(sourceLanguage) {
|
||||
const sourceTranslation = this.getTranslation(sourceLanguage);
|
||||
const targetTranslation = this.getTranslation(this.activeContentLanguage);
|
||||
|
||||
if (!sourceTranslation || !targetTranslation) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.assign(targetTranslation, {
|
||||
author: sourceTranslation.author,
|
||||
title: sourceTranslation.title,
|
||||
text: sourceTranslation.text,
|
||||
});
|
||||
|
||||
this.contentFormRenderKey += 1;
|
||||
this.$emit('translation-copied', {
|
||||
from: sourceLanguage,
|
||||
to: this.activeContentLanguage,
|
||||
});
|
||||
},
|
||||
handleContentTabChange(key) {
|
||||
if (key === 'addLanguage') {
|
||||
this.showLanguageModal();
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeContentFormKey = key;
|
||||
},
|
||||
handleContentTabAction({ key, payload }) {
|
||||
if (key === 'addLanguage') {
|
||||
this.showLanguageModal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload?.action === 'remove-language') {
|
||||
this.removeLanguage(payload.language);
|
||||
}
|
||||
},
|
||||
removeLanguage(language) {
|
||||
const key = this.getContentFormKey(language);
|
||||
|
||||
if (!this.contentFormItems[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentFormItems = { ...this.contentFormItems };
|
||||
const addLanguageTab = contentFormItems.addLanguage;
|
||||
delete contentFormItems[key];
|
||||
delete contentFormItems.addLanguage;
|
||||
|
||||
const remainingLanguages = Object.values(contentFormItems)
|
||||
.map((item) => item.config?.language)
|
||||
.filter(Boolean);
|
||||
contentFormItems.addLanguage = {
|
||||
...addLanguageTab,
|
||||
config: {
|
||||
hasAvailableLanguages: this.availableLanguages.some(
|
||||
(availableLanguage) =>
|
||||
!remainingLanguages.includes(availableLanguage.value),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
this.contentFormItems = contentFormItems;
|
||||
this.formData.translations = this.formData.translations.filter(
|
||||
(translation) => translation.language !== language,
|
||||
);
|
||||
|
||||
const nextContentFormKey = Object.keys(contentFormItems).find(
|
||||
(itemKey) => itemKey !== 'addLanguage',
|
||||
);
|
||||
this.activeContentFormKey = nextContentFormKey ?? 'addLanguage';
|
||||
|
||||
if (nextContentFormKey) {
|
||||
this.$nextTick(() => this.$refs.tabs.change(nextContentFormKey));
|
||||
}
|
||||
},
|
||||
addLanguage(language) {
|
||||
const key = this.getContentFormKey(language);
|
||||
|
||||
if (this.contentFormItems[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addLanguageTab = this.contentFormItems.addLanguage;
|
||||
const contentFormItems = { ...this.contentFormItems };
|
||||
delete contentFormItems.addLanguage;
|
||||
contentFormItems[key] = this.createContentFormItem(language);
|
||||
contentFormItems.addLanguage = {
|
||||
...addLanguageTab,
|
||||
config: {
|
||||
hasAvailableLanguages: this.availableLanguages.some(
|
||||
(availableLanguage) =>
|
||||
availableLanguage.value !== language &&
|
||||
!this.visibleContentLanguages.includes(availableLanguage.value),
|
||||
),
|
||||
},
|
||||
};
|
||||
this.contentFormItems = contentFormItems;
|
||||
|
||||
if (
|
||||
!this.formData.translations.some((item) => item.language === language)
|
||||
) {
|
||||
this.formData.translations.push({
|
||||
language,
|
||||
author: '',
|
||||
title: '',
|
||||
text: '',
|
||||
isPublished: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.activeContentFormKey = key;
|
||||
this.$refs.addLanguageModal.hide();
|
||||
this.$nextTick(() => this.$refs.tabs.change(key));
|
||||
},
|
||||
restoreActiveContentTab() {
|
||||
this.$refs.tabs.change(this.activeContentFormKey);
|
||||
},
|
||||
fillFormData(news) {
|
||||
if (!news) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.formData.visibleFrom =
|
||||
news.visibleFrom ?? news.dateTime?.slice(0, 10) ?? null;
|
||||
this.formData.visibleTo = news.visibleTo ?? null;
|
||||
const degreeProgramShortCode =
|
||||
news.degreeProgramShortCode ?? news.degreeProgram?.value ?? null;
|
||||
this.formData.degreeProgram =
|
||||
this.dropdownParsedDegreePrograms.find(
|
||||
(degreeProgram) =>
|
||||
String(degreeProgram.value) === String(degreeProgramShortCode),
|
||||
) ??
|
||||
(degreeProgramShortCode === null
|
||||
? null
|
||||
: {
|
||||
label: String(degreeProgramShortCode),
|
||||
value: degreeProgramShortCode,
|
||||
});
|
||||
this.formData.semester = news.semester ?? null;
|
||||
|
||||
const translations = news.translations ?? [
|
||||
{
|
||||
language: news.language,
|
||||
author: news.author,
|
||||
title: news.title,
|
||||
text: news.content,
|
||||
isPublished: news.isPublished,
|
||||
},
|
||||
];
|
||||
|
||||
translations.forEach((sourceTranslation) => {
|
||||
const translation = this.formData.translations.find(
|
||||
(item) => item.language === sourceTranslation.language,
|
||||
);
|
||||
|
||||
if (translation) {
|
||||
Object.assign(translation, sourceTranslation);
|
||||
return;
|
||||
}
|
||||
|
||||
this.formData.translations.push({
|
||||
language: sourceTranslation.language,
|
||||
author: sourceTranslation.author ?? '',
|
||||
title: sourceTranslation.title ?? '',
|
||||
text: sourceTranslation.text ?? '',
|
||||
isPublished: Boolean(sourceTranslation.isPublished),
|
||||
});
|
||||
});
|
||||
},
|
||||
filterDegreePrograms(event) {
|
||||
let defaultItem = {
|
||||
label: this.$p.t('ui', 'dropdownEmptyOption'),
|
||||
value: null,
|
||||
};
|
||||
|
||||
const query = event.query.toLowerCase();
|
||||
if (!query) {
|
||||
return (this.filteredDegreePrograms = [
|
||||
defaultItem,
|
||||
...this.dropdownParsedDegreePrograms,
|
||||
]);
|
||||
}
|
||||
|
||||
return (this.filteredDegreePrograms = [defaultItem]
|
||||
.concat(this.dropdownParsedDegreePrograms)
|
||||
.filter((unit) => {
|
||||
return unit.label.toLowerCase().includes(query);
|
||||
}));
|
||||
},
|
||||
async storeNewsItem() {
|
||||
let parsedFormData = JSON.parse(JSON.stringify(this.formData));
|
||||
parsedFormData.degreeProgramShortCode =
|
||||
parsedFormData.degreeProgram?.value;
|
||||
delete parsedFormData.degreeProgram;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await this.$refs.form.call(
|
||||
ApiNewsAdministration.storeNewsItem(parsedFormData),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error storing news item:', error);
|
||||
this.$fhcAlert.handleSystemError(error);
|
||||
return;
|
||||
}
|
||||
if (response.meta.status !== 'success') {
|
||||
this.$fhcAlert.alertError(this.$p.t('ui', 'errorStoringNewsItem'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.$fhcAlert.alertSuccess(
|
||||
this.$p.t('ui', 'newsItemStoredSuccessfully'),
|
||||
);
|
||||
this.$emit('created');
|
||||
},
|
||||
async updateNewsItem() {
|
||||
if (!this.news || this.isSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedFormData = JSON.parse(JSON.stringify(this.formData));
|
||||
parsedFormData.degreeProgramShortCode =
|
||||
parsedFormData.degreeProgram?.value;
|
||||
delete parsedFormData.degreeProgram;
|
||||
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
const response = await this.$refs.form.call(
|
||||
ApiNewsAdministration.updateNewsItem(
|
||||
this.news.newsId,
|
||||
parsedFormData,
|
||||
),
|
||||
);
|
||||
|
||||
if (response.meta.status !== 'success') {
|
||||
this.$fhcAlert.alertError(this.$p.t('ui', 'errorStoringNewsItem'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.$fhcAlert.alertSuccess(
|
||||
this.$p.t('ui', 'newsItemStoredSuccessfully'),
|
||||
);
|
||||
this.$emit('update');
|
||||
} catch (error) {
|
||||
if (!error?.handled) {
|
||||
this.$fhcAlert.handleSystemError(error);
|
||||
}
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
this.fillFormData(this.news);
|
||||
|
||||
let getAllDegreePrograms = await this.$api.call(
|
||||
ApiStudiengang.getDegreePrograms(),
|
||||
);
|
||||
if (getAllDegreePrograms.meta.status === 'success') {
|
||||
this.degreePrograms = getAllDegreePrograms.data.sort((a, b) =>
|
||||
a.bezeichnung.localeCompare(b.bezeichnung),
|
||||
);
|
||||
this.fillFormData(this.news);
|
||||
} else {
|
||||
this.$fhcAlert.alertError(this.$p.t('ui', 'errorFetchingDegreePrograms'));
|
||||
}
|
||||
|
||||
console.log(FHC_JS_DATA_STORAGE_OBJECT);
|
||||
},
|
||||
template: /*html*/ `
|
||||
<section
|
||||
:class="{'pb-3': isMobile}"
|
||||
class="card bg-white shadow-sm border-0 mb-4 overflow-x-hidden"
|
||||
aria-labelledby="news-item-form-heading"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center gap-3 mb-3">
|
||||
<h4 id="news-item-form-heading" ref="newsPageHeading" class="card-title fhc-primary-color mb-0">News Form</h4>
|
||||
<button
|
||||
@click="$emit('toggle-preview')"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary d-inline-flex align-items-center justify-content-center"
|
||||
:title="isPreviewShown ? 'Hide preview' : 'Show preview'"
|
||||
:aria-label="isPreviewShown ? 'Hide preview' : 'Show preview'"
|
||||
:aria-expanded="isPreviewShown"
|
||||
aria-controls="news-item-preview"
|
||||
>
|
||||
<i class="fa-solid" :class="isPreviewShown ? 'fa-eye-slash' : 'fa-eye'" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<core-form ref="form">
|
||||
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
|
||||
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
|
||||
<form-input
|
||||
:label="$p.t('ui', 'visible') + ' ' + $p.t('ui', 'von')"
|
||||
:teleport="true"
|
||||
:enable-time-picker="false"
|
||||
type="datePicker"
|
||||
name="visibleFrom"
|
||||
v-model="formData.visibleFrom"
|
||||
model-type="yyyy-MM-dd"
|
||||
format="dd.MM.yyyy"
|
||||
auto-apply
|
||||
/>
|
||||
<form-input
|
||||
:label="$p.t('ui', 'visible') + ' ' + $p.t('global', 'bis')"
|
||||
:teleport="true"
|
||||
:enable-time-picker="false"
|
||||
type="datePicker"
|
||||
name="visibleTo"
|
||||
v-model="formData.visibleTo"
|
||||
model-type="yyyy-MM-dd"
|
||||
format="dd.MM.yyyy"
|
||||
auto-apply
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
|
||||
<form-input
|
||||
:label="$capitalize($p.t('lehre/studiengang'))"
|
||||
:suggestions="filteredDegreePrograms"
|
||||
:optionValue="(option) => option.value"
|
||||
:optionLabel="(option) => option.label"
|
||||
@complete="filterDegreePrograms($event)"
|
||||
type="autocomplete"
|
||||
name="degreeProgramShortCode"
|
||||
v-model="formData.degreeProgram"
|
||||
dropdown
|
||||
forceSelection
|
||||
>
|
||||
</form-input>
|
||||
<form-input
|
||||
v-model="formData.semester"
|
||||
:label="$capitalize($p.t('lehre/studiensemester'))"
|
||||
type="select"
|
||||
name="semester"
|
||||
>
|
||||
<option
|
||||
v-for="semester in semesters"
|
||||
:key="semester.value"
|
||||
:value="semester.value"
|
||||
>
|
||||
{{ semester.label }}
|
||||
</option>
|
||||
</form-input>
|
||||
</div>
|
||||
</div>
|
||||
<fhc-tabs
|
||||
:key="contentFormRenderKey"
|
||||
ref="tabs"
|
||||
:useprimevue="true"
|
||||
:config="contentFormItems"
|
||||
:default="activeContentFormKey"
|
||||
v-model="formData.translations"
|
||||
@change="handleContentTabChange"
|
||||
@tab-action="handleContentTabAction"
|
||||
style="flex: 1 1 0%; height: 0%"
|
||||
class="mt-3"
|
||||
>
|
||||
</fhc-tabs>
|
||||
<div class="d-flex justify-content-end mt-2">
|
||||
<button
|
||||
v-if="activeContentLanguage && copySourceLanguages.length"
|
||||
@click="showCopyTranslationModal"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
:title="copySourceLanguages.length ? 'Copy content from another language' : 'Add another language to copy content'"
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
<i class="fa-solid fa-copy me-1" aria-hidden="true"></i>
|
||||
Copy content from another language
|
||||
</button>
|
||||
</div>
|
||||
<news-item-add-language-modal
|
||||
ref="addLanguageModal"
|
||||
:languages="languagesToAdd"
|
||||
@add-language="addLanguage"
|
||||
@hidden="restoreActiveContentTab"
|
||||
></news-item-add-language-modal>
|
||||
<news-item-copy-translation-modal
|
||||
ref="copyTranslationModal"
|
||||
:source-languages="copySourceLanguages"
|
||||
:target-language-label="getLanguageLabel(activeContentLanguage)"
|
||||
:target-has-content="activeTranslationHasContent"
|
||||
@copy="copyTranslation"
|
||||
></news-item-copy-translation-modal>
|
||||
<div class="d-flex justify-content-end gap-2 mt-4 pt-3 border-top">
|
||||
<button
|
||||
@click="$emit('cancel')"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
{{$p.t('ui', 'abbrechen')}}
|
||||
</button>
|
||||
<button
|
||||
@click="news ? updateNewsItem() : storeNewsItem()"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
{{$p.t('global', 'speichern')}}
|
||||
</button>
|
||||
</div>
|
||||
</core-form>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import FormInput from '../../../Form/Input.js';
|
||||
|
||||
export default {
|
||||
name: 'NewsItemPreview',
|
||||
components: {
|
||||
FormInput,
|
||||
},
|
||||
props: {
|
||||
formData: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
activeLanguage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLanguage: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
availableLanguages() {
|
||||
return (this.formData?.translations ?? [])
|
||||
.map((translation) => translation.language)
|
||||
.filter(Boolean);
|
||||
},
|
||||
displayedLanguage() {
|
||||
return this.selectedLanguage || this.activeLanguage;
|
||||
},
|
||||
translation() {
|
||||
return this.formData?.translations?.find(
|
||||
(item) => item.language === this.displayedLanguage,
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeLanguage: {
|
||||
immediate: true,
|
||||
handler(language) {
|
||||
if (this.availableLanguages.includes(language)) {
|
||||
this.selectedLanguage = language;
|
||||
}
|
||||
},
|
||||
},
|
||||
availableLanguages(languages) {
|
||||
if (languages.includes(this.selectedLanguage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedLanguage = languages.includes(this.activeLanguage)
|
||||
? this.activeLanguage
|
||||
: (languages[0] ?? '');
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<section
|
||||
id="news-item-preview"
|
||||
class="card bg-white shadow-sm border-0 mb-4 position-sticky"
|
||||
style="top: 1rem"
|
||||
aria-labelledby="news-item-preview-heading"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center gap-3 mb-3">
|
||||
<h4 id="news-item-preview-heading" class="card-title fhc-primary-color mb-0">News preview</h4>
|
||||
<form-input
|
||||
v-if="availableLanguages.length > 1"
|
||||
v-model="selectedLanguage"
|
||||
type="select"
|
||||
name="previewLanguage"
|
||||
class="m-0"
|
||||
>
|
||||
<option
|
||||
v-for="language in availableLanguages"
|
||||
:key="language"
|
||||
:value="language"
|
||||
>
|
||||
{{ language }}
|
||||
</option>
|
||||
</form-input>
|
||||
</div>
|
||||
<article v-if="translation" class="card border">
|
||||
<header class="card-header" :class="translation.isPublished ? 'fhc-primary' : 'bg-secondary bg-opacity-25 text-secondary'">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||
<h5 class="mb-0">{{ translation.title || 'Untitled news' }}</h5>
|
||||
<span v-if="!translation.isPublished" class="badge text-bg-secondary">Not published yet</span>
|
||||
</div>
|
||||
<address v-if="translation.author" class="small mb-0 mt-2">{{ translation.author }}</address>
|
||||
</header>
|
||||
<div class="card-body">
|
||||
<div v-if="translation.text" class="card-text" v-html="translation.text"></div>
|
||||
<p v-else class="text-muted mb-0">Enter text to see the news content preview.</p>
|
||||
</div>
|
||||
</article>
|
||||
<p v-else class="text-muted mb-0">Select a language tab to preview its content.</p>
|
||||
</div>
|
||||
</section>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
import Pagination from '../../../Pagination/Pagination.js';
|
||||
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
|
||||
import NewsListItem from './NewsListItem.js';
|
||||
|
||||
const MAX_NEW_AGE = 60;
|
||||
|
||||
export default {
|
||||
name: 'NewsList',
|
||||
emits: ['edit'],
|
||||
components: {
|
||||
NewsListItem,
|
||||
Pagination,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newsItems: [],
|
||||
isLoading: false,
|
||||
loadError: null,
|
||||
maxPageCount: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'$p.user_language.value': function () {
|
||||
this.page = 1;
|
||||
this.fetchNews();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sprache() {
|
||||
return this.$p.user_language.value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getNewsItem(newsId) {
|
||||
await this.fetchNews();
|
||||
|
||||
return this.newsItems.find(
|
||||
(news) => String(news.newsId) === String(newsId),
|
||||
);
|
||||
},
|
||||
async fetchNews() {
|
||||
this.isLoading = true;
|
||||
this.loadError = null;
|
||||
|
||||
try {
|
||||
const response = await this.$api.call(
|
||||
ApiNewsAdministration.getNews(
|
||||
this.page,
|
||||
this.pageSize,
|
||||
this.sprache,
|
||||
MAX_NEW_AGE,
|
||||
'all',
|
||||
),
|
||||
);
|
||||
this.newsItems = this.parseNewsHtml(response.data);
|
||||
this.maxPageCount = response.meta.row_count;
|
||||
} catch (error) {
|
||||
this.newsItems = [];
|
||||
this.loadError = this.$p.t('global', 'fehlerBeimLadenDesDatensatzes');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
parseNewsHtml(html) {
|
||||
if (!html) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const document = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
return Array.from(document.querySelectorAll('.news-list-item')).map(
|
||||
(article) => this.parseNewsItem(article),
|
||||
);
|
||||
},
|
||||
parseNewsItem(article) {
|
||||
console.log('Parsing news item:', article);
|
||||
const newsId = article
|
||||
.querySelector('[news-id]')
|
||||
?.getAttribute('news-id');
|
||||
const title = article.querySelector('.card-header h2');
|
||||
const author = article.querySelector('address');
|
||||
const time = article.querySelector('time');
|
||||
const isPublished = article.querySelector(
|
||||
'.card-header span[is-published]',
|
||||
);
|
||||
const content = article.querySelector('.card-text');
|
||||
|
||||
return {
|
||||
newsId: newsId ? Number(newsId) : null,
|
||||
language: this.sprache,
|
||||
title: title ? title.textContent.trim() : '',
|
||||
author: author ? author.textContent.trim() : '',
|
||||
date: time ? time.textContent.trim() : '',
|
||||
dateTime: time ? time.getAttribute('datetime') || '' : '',
|
||||
content: content ? content.innerHTML : '',
|
||||
isPublished: isPublished
|
||||
? isPublished.getAttribute('is-published') === 'true'
|
||||
: null,
|
||||
};
|
||||
},
|
||||
afterPageUpdated(event) {
|
||||
this.page = event.page;
|
||||
this.pageSize = event.rows;
|
||||
this.fetchNews();
|
||||
this.$refs.newsListHeading.scrollIntoView({ block: 'end' });
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchNews();
|
||||
},
|
||||
template: /*html*/ `
|
||||
<section class="mt-4" aria-labelledby="news-administration-list-heading">
|
||||
<h3 id="news-administration-list-heading" ref="newsListHeading" class="fhc-primary-color">
|
||||
News
|
||||
</h3>
|
||||
<hr>
|
||||
<pagination
|
||||
v-if="maxPageCount > pageSize"
|
||||
:page="page"
|
||||
:page_size="pageSize"
|
||||
:maxPageCount="maxPageCount"
|
||||
@pageUpdated="afterPageUpdated"
|
||||
></pagination>
|
||||
<div v-if="isLoading" class="d-flex justify-content-center py-4" role="status">
|
||||
<span class="spinner-border" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Loading news</span>
|
||||
</div>
|
||||
<div v-else-if="loadError" class="alert alert-danger" role="alert">
|
||||
{{ loadError }}
|
||||
</div>
|
||||
<div v-else-if="!newsItems.length" class="alert alert-info" role="status">
|
||||
No news available.
|
||||
</div>
|
||||
<div v-else>
|
||||
<news-list-item
|
||||
v-for="(news, index) in newsItems"
|
||||
:key="news.newsId || page + '-' + index"
|
||||
:news="news"
|
||||
@edit="$emit('edit', $event)"
|
||||
@deleted="fetchNews()"
|
||||
></news-list-item>
|
||||
</div>
|
||||
<pagination
|
||||
v-if="maxPageCount > pageSize"
|
||||
:page="page"
|
||||
:page_size="pageSize"
|
||||
:maxPageCount="maxPageCount"
|
||||
@pageUpdated="afterPageUpdated"
|
||||
></pagination>
|
||||
</section>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import ApiNewsAdministration from '../../../../api/factory/newsAdministration.js';
|
||||
|
||||
const createCollapseId = () => {
|
||||
const randomNumber = Math.floor(
|
||||
1000000000000000 + Math.random() * 8000000000000000,
|
||||
);
|
||||
return `news-administration-item-${randomNumber}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'NewsListItem',
|
||||
emits: ['edit', 'deleted'],
|
||||
props: {
|
||||
news: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExpanded: false,
|
||||
isDeleting: false,
|
||||
collapseId: createCollapseId(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleContent() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
},
|
||||
editNews(news) {
|
||||
this.$emit('edit', news);
|
||||
},
|
||||
async deleteNews(news) {
|
||||
if (this.isDeleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('ui', 'frageSicherLoeschen'),
|
||||
acceptClass: 'btn btn-danger',
|
||||
})) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDeleting = true;
|
||||
|
||||
try {
|
||||
const response = await this.$api.call(
|
||||
ApiNewsAdministration.deleteNewsItem(news.newsId),
|
||||
);
|
||||
|
||||
if (response.meta.status !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('deleted', news);
|
||||
} catch (error) {
|
||||
this.$fhcAlert.handleSystemError(error);
|
||||
} finally {
|
||||
this.isDeleting = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<article
|
||||
class="card mb-3"
|
||||
:class="{'border-secondary': !news.isPublished}"
|
||||
>
|
||||
<header
|
||||
class="card-header d-flex justify-content-between align-items-center gap-3"
|
||||
:class="news.isPublished ? 'fhc-primary' : 'bg-secondary bg-opacity-25 text-secondary'"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-reset text-decoration-none text-start d-flex align-items-center gap-2 flex-grow-1 p-0"
|
||||
:aria-expanded="isExpanded"
|
||||
:aria-controls="collapseId"
|
||||
@click="toggleContent"
|
||||
>
|
||||
<i
|
||||
class="fa-solid"
|
||||
:class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-right'"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="h5 mb-0">{{ news.title }}</span>
|
||||
<span v-if="!news.isPublished" class="badge text-bg-secondary">
|
||||
Not published yet
|
||||
</span>
|
||||
</button>
|
||||
<div class="d-flex align-items-center gap-2 flex-shrink-0">
|
||||
<div class="text-end">
|
||||
<address v-if="news.author" class="fw-bold small mb-0">
|
||||
{{ news.author }}
|
||||
</address>
|
||||
<time v-if="news.date" class="small" :datetime="news.dateTime">
|
||||
{{ news.date }}
|
||||
</time>
|
||||
</div>
|
||||
<div class="d-flex gap-1" aria-label="News actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm rounded-circle shadow-sm d-inline-flex align-items-center justify-content-center"
|
||||
:class="news.isPublished ? 'btn-light text-primary' : 'btn-outline-secondary bg-white'"
|
||||
style="width: 1.75rem; height: 1.75rem"
|
||||
title="Edit news"
|
||||
aria-label="Edit news"
|
||||
@click="editNews(news)"
|
||||
>
|
||||
<i class="fa-solid fa-pen" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm rounded-circle shadow-sm d-inline-flex align-items-center justify-content-center"
|
||||
:class="news.isPublished ? 'btn-light text-danger' : 'btn-outline-danger bg-white'"
|
||||
style="width: 1.75rem; height: 1.75rem"
|
||||
title="Delete news"
|
||||
aria-label="Delete news"
|
||||
:disabled="isDeleting"
|
||||
@click="deleteNews(news)"
|
||||
>
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div v-show="isExpanded" :id="collapseId" class="card-body">
|
||||
<div class="card-text" v-html="news.content"></div>
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
|
||||
import FormInput from "../../Form/Input.js";
|
||||
|
||||
export default {
|
||||
name: "NewsItemContentForm",
|
||||
components: {
|
||||
FormInput,
|
||||
},
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initTinyMCE() {
|
||||
const vm = this;
|
||||
tinymce.init({
|
||||
target: this.$refs.editor.$refs.input, //Important: not selector: to enable multiple import of component
|
||||
//height: 800,
|
||||
//plugins: ['lists'],
|
||||
//toolbar: " blocks | bold italic underline | alignleft aligncenter alignright alignjustify",
|
||||
toolbar: 'styleselect | bold italic underline | alignleft aligncenter alignright alignjustify',
|
||||
style_formats: [
|
||||
{title: 'Blocks', block: 'div'},
|
||||
{title: 'Paragraph', block: 'p'},
|
||||
{title: 'Heading 1', block: 'h1'},
|
||||
{title: 'Heading 2', block: 'h2'},
|
||||
{title: 'Heading 3', block: 'h3'},
|
||||
{title: 'Heading 4', block: 'h4'},
|
||||
{title: 'Heading 5', block: 'h5'},
|
||||
{title: 'Heading 6', block: 'h6'},
|
||||
],
|
||||
autoresize_bottom_margin: 16,
|
||||
|
||||
setup: (editor) => {
|
||||
vm.editor = editor;
|
||||
|
||||
editor.on('input', () => {
|
||||
const newContent = editor.getContent();
|
||||
vm.notizData.text = newContent;
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initTinyMCE();
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-end gap-3">
|
||||
<form-input
|
||||
type="text"
|
||||
:label="$p.t('wawi/nummer')"
|
||||
name="nummer"
|
||||
>
|
||||
</form-input>
|
||||
<form-input
|
||||
type="text"
|
||||
:label="$p.t('wawi/nummer')"
|
||||
name="nummer"
|
||||
>
|
||||
</form-input>
|
||||
<form-input
|
||||
type="checkbox"
|
||||
:label="$p.t('wawi/nummer')"
|
||||
name="nummer"
|
||||
>
|
||||
</form-input>
|
||||
</div>
|
||||
<form-input
|
||||
ref="editor"
|
||||
:label="$p.t('global','text') + ' *'"
|
||||
type="textarea"
|
||||
name="text"
|
||||
rows="5"
|
||||
cols="75"
|
||||
>
|
||||
>
|
||||
</form-input>
|
||||
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import CoreForm from "../../Form/Form.js";
|
||||
import FormInput from "../../Form/Input.js";
|
||||
import FhcTabs from "../../Tabs.js";
|
||||
|
||||
const BASE_COMPONENT_URL =
|
||||
"https://c3p0.dev.technikum-wien.at/ma1433/core/FHC-Core/public/js/components/Cis/Cms/";
|
||||
|
||||
export default {
|
||||
name: "NewsItemForm",
|
||||
components: {
|
||||
CoreForm,
|
||||
FormInput,
|
||||
FhcTabs,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contentFormItems: {
|
||||
germanContentForm: {
|
||||
title: "German Content Form",
|
||||
component: BASE_COMPONENT_URL + "NewsItemContentForm.js?123",
|
||||
config: {
|
||||
language: "de",
|
||||
type: "news",
|
||||
},
|
||||
key: "germanContentForm",
|
||||
},
|
||||
englishContentForm: {
|
||||
title: "English Content Form",
|
||||
component: BASE_COMPONENT_URL + "NewsItemContentForm.js?1233",
|
||||
config: {
|
||||
language: "en",
|
||||
type: "news",
|
||||
},
|
||||
key: "englishContentForm",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
"$p.user_language.value": function (sprache) {
|
||||
this.fetchNews();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sprache: function () {
|
||||
return this.$p.user_language.value;
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
created() {},
|
||||
template: /*html*/ `
|
||||
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
|
||||
<h2 ref="newsPageHeading" class="fhc-primary-color">News Form</h2>
|
||||
<div>
|
||||
<core-form>
|
||||
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
|
||||
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
|
||||
<form-input
|
||||
:label="$p.t('ui', 'validityPeriod') + ' ' + $p.t('global', 'bis')"
|
||||
:teleport="true"
|
||||
:enable-time-picker="false"
|
||||
type="datePicker"
|
||||
name="validityPeriodTo"
|
||||
format="dd.MM.yyyy"
|
||||
auto-apply
|
||||
/>
|
||||
<form-input
|
||||
:label="$p.t('ui', 'validityPeriod') + ' ' + $p.t('global', 'bis')"
|
||||
:teleport="true"
|
||||
:enable-time-picker="false"
|
||||
type="datePicker"
|
||||
name="validityPeriodTo"
|
||||
format="dd.MM.yyyy"
|
||||
auto-apply
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-row flex-md-row align-items-md-end gap-3">
|
||||
<form-input
|
||||
:label="$capitalize($p.t('lehre/studiensemester'))"
|
||||
:suggestions="filteredSemesters"
|
||||
:optionValue="(option) => option.value"
|
||||
:optionLabel="(option) => option.label"
|
||||
@complete="filterSemesters($event)"
|
||||
type="autocomplete"
|
||||
name="selectedSemester"
|
||||
dropdown
|
||||
forceSelection
|
||||
>
|
||||
</form-input>
|
||||
<form-input
|
||||
:label="$capitalize($p.t('lehre/studiensemester'))"
|
||||
:suggestions="filteredSemesters"
|
||||
:optionValue="(option) => option.value"
|
||||
:optionLabel="(option) => option.label"
|
||||
@complete="filterSemesters($event)"
|
||||
type="autocomplete"
|
||||
name="selectedSemester"
|
||||
dropdown
|
||||
forceSelection
|
||||
>
|
||||
</form-input>
|
||||
</div>
|
||||
</div>
|
||||
<fhc-tabs
|
||||
ref="tabs"
|
||||
:useprimevue="true"
|
||||
:config="contentFormItems"
|
||||
style="flex: 1 1 0%; height: 0%"
|
||||
class="mt-3"
|
||||
>
|
||||
</fhc-tabs>
|
||||
</core-form>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
import CoreForm from "../../Form/Form.js";
|
||||
import FormInput from "../../Form/Input.js";
|
||||
import Tabs from "../../Tabs.js";
|
||||
|
||||
export default {
|
||||
name: "NewsItemForm",
|
||||
components: {
|
||||
CoreForm,
|
||||
FormInput,
|
||||
Tabs,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
"$p.user_language.value": function (sprache) {
|
||||
this.fetchNews();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
sprache: function () {
|
||||
return this.$p.user_language.value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
created() {
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
|
||||
test
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -7,18 +7,17 @@ const MAX_LOADED_NEWS = 30;
|
||||
const MAX_NEW_AGE = 60;
|
||||
|
||||
export default {
|
||||
name: "WidgetsNews",
|
||||
name: 'WidgetsNews',
|
||||
components: {
|
||||
BsModal
|
||||
BsModal,
|
||||
},
|
||||
data: () => ({
|
||||
allNewsList: [],
|
||||
singleNews: {},
|
||||
selected: null,
|
||||
size:0,
|
||||
size: 0,
|
||||
}),
|
||||
props: ['width', 'height']
|
||||
,
|
||||
props: ['width', 'height'],
|
||||
mixins: [AbstractWidget],
|
||||
computed: {
|
||||
sizeClass() {
|
||||
@@ -29,7 +28,6 @@ export default {
|
||||
// let quantity = this.width;
|
||||
let quantity = MAX_LOADED_NEWS;
|
||||
|
||||
|
||||
if (this.width === 1) {
|
||||
quantity = this.height === 1 ? 4 : MAX_LOADED_NEWS;
|
||||
}
|
||||
@@ -40,107 +38,129 @@ export default {
|
||||
},
|
||||
carouselItems() {
|
||||
return this.allNewsList.reduce((acc, cur) => {
|
||||
const el = document.getElementById('card-'+cur.news_id)
|
||||
const el = document.getElementById('card-' + cur.news_id);
|
||||
acc.push(el);
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateNewsContentClasses:function(){
|
||||
updateNewsContentClasses: function () {
|
||||
Vue.nextTick(() => {
|
||||
document.querySelectorAll(".fhc-news-card-item .card-body, .fhc-news-card-item .card, .fhc-news-card-item .card-header").forEach((el) => {
|
||||
if (!el.classList.contains("border-0")) {
|
||||
el.classList.add("border-0");
|
||||
}
|
||||
});
|
||||
document.querySelectorAll(".fhc-news-card-item .card-header").forEach((el) => {
|
||||
if (!el.classList.contains("px-5")) {
|
||||
el.classList.add("px-5");
|
||||
}
|
||||
if (!el.classList.contains("fhc-primary")) {
|
||||
el.classList.add("fhc-primary");
|
||||
}
|
||||
if (!el.classList.contains("position-sticky")) {
|
||||
el.classList.add("position-sticky");
|
||||
}
|
||||
if (!el.classList.contains("top-0")) {
|
||||
el.classList.add("top-0");
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
document.querySelectorAll(".fhc-news-card-item .card-header .row").forEach((el) => {
|
||||
if (!el.classList.contains("w-100")) {
|
||||
el.classList.add("w-100");
|
||||
}
|
||||
if (!el.classList.contains("align-items-center")) {
|
||||
el.classList.add("align-items-center");
|
||||
}
|
||||
});
|
||||
document.querySelectorAll(".fhc-news-card-item .card-header .row h2").forEach((el) => {
|
||||
if (!el.classList.contains("mb-0")) {
|
||||
el.classList.add("mb-0");
|
||||
}
|
||||
});
|
||||
})
|
||||
document
|
||||
.querySelectorAll(
|
||||
'.fhc-news-card-item .card-body, .fhc-news-card-item .card, .fhc-news-card-item .card-header',
|
||||
)
|
||||
.forEach((el) => {
|
||||
if (!el.classList.contains('border-0')) {
|
||||
el.classList.add('border-0');
|
||||
}
|
||||
});
|
||||
document
|
||||
.querySelectorAll('.fhc-news-card-item .card-header')
|
||||
.forEach((el) => {
|
||||
if (!el.classList.contains('px-5')) {
|
||||
el.classList.add('px-5');
|
||||
}
|
||||
if (!el.classList.contains('fhc-primary')) {
|
||||
el.classList.add('fhc-primary');
|
||||
}
|
||||
if (!el.classList.contains('position-sticky')) {
|
||||
el.classList.add('position-sticky');
|
||||
}
|
||||
if (!el.classList.contains('top-0')) {
|
||||
el.classList.add('top-0');
|
||||
}
|
||||
});
|
||||
document
|
||||
.querySelectorAll('.fhc-news-card-item .card-header .row')
|
||||
.forEach((el) => {
|
||||
if (!el.classList.contains('w-100')) {
|
||||
el.classList.add('w-100');
|
||||
}
|
||||
if (!el.classList.contains('align-items-center')) {
|
||||
el.classList.add('align-items-center');
|
||||
}
|
||||
});
|
||||
document
|
||||
.querySelectorAll('.fhc-news-card-item .card-header .row h2')
|
||||
.forEach((el) => {
|
||||
if (!el.classList.contains('mb-0')) {
|
||||
el.classList.add('mb-0');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
formatDate: function (dateTime) {
|
||||
const dt = new Date(dateTime);
|
||||
return numberPadding(dt.getDate()) + '.' + numberPadding((dt.getMonth() + 1)) + '.' + dt.getFullYear();
|
||||
return (
|
||||
numberPadding(dt.getDate()) +
|
||||
'.' +
|
||||
numberPadding(dt.getMonth() + 1) +
|
||||
'.' +
|
||||
dt.getFullYear()
|
||||
);
|
||||
},
|
||||
formatTime: function (dateTime) {
|
||||
const dt = new Date(dateTime);
|
||||
return numberPadding(dt.getHours()) + ':' + numberPadding(dt.getMinutes());
|
||||
return (
|
||||
numberPadding(dt.getHours()) + ':' + numberPadding(dt.getMinutes())
|
||||
);
|
||||
},
|
||||
isString(value){
|
||||
isString(value) {
|
||||
return Object.prototype.toString.call(value) === '[object String]';
|
||||
},
|
||||
setNext(){
|
||||
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
|
||||
const nextIndex = thisIndex == (this.allNewsList.length - 1) ? 0 : thisIndex + 1
|
||||
setNext() {
|
||||
const thisIndex = this.allNewsList.findIndex(
|
||||
(n) => n.news_id == this.selected.news_id,
|
||||
);
|
||||
const nextIndex =
|
||||
thisIndex == this.allNewsList.length - 1 ? 0 : thisIndex + 1;
|
||||
|
||||
this.setSelected(this.allNewsList[nextIndex])
|
||||
this.setSelected(this.allNewsList[nextIndex]);
|
||||
this.updateNewsContentClasses();
|
||||
|
||||
},
|
||||
setPrev() {
|
||||
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
|
||||
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1
|
||||
const thisIndex = this.allNewsList.findIndex(
|
||||
(n) => n.news_id == this.selected.news_id,
|
||||
);
|
||||
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1;
|
||||
|
||||
this.setSelected(this.allNewsList[prevIndex])
|
||||
this.setSelected(this.allNewsList[prevIndex]);
|
||||
this.updateNewsContentClasses();
|
||||
},
|
||||
getMenuItemClass(news) {
|
||||
let classString = ''
|
||||
if(this.selected && this.selected.news_id === news.news_id) {
|
||||
classString += 'selected'
|
||||
let classString = '';
|
||||
if (this.selected && this.selected.news_id === news.news_id) {
|
||||
classString += 'selected';
|
||||
}
|
||||
return classString
|
||||
return classString;
|
||||
},
|
||||
async setSelected(news) {
|
||||
const clickedElement = document.getElementById('card-'+news.news_id);
|
||||
const clickedElement = document.getElementById('card-' + news.news_id);
|
||||
const clickedElementIndex = this.allNewsList.indexOf(news);
|
||||
const oldElementIndex = this.allNewsList.indexOf(this.selected);
|
||||
|
||||
|
||||
//if the clicked element is already active, do nothing
|
||||
if(clickedElementIndex === oldElementIndex) return;
|
||||
if (clickedElementIndex === oldElementIndex) return;
|
||||
//add prev/next class to the clicked element
|
||||
if(clickedElementIndex > oldElementIndex) {
|
||||
if (clickedElementIndex > oldElementIndex) {
|
||||
clickedElement.classList.add('carousel-item-next');
|
||||
}else{
|
||||
} else {
|
||||
clickedElement.classList.add('carousel-item-prev');
|
||||
}
|
||||
|
||||
// move to clicked element
|
||||
await Vue.nextTick(() => { this.carouselInstance.to(clickedElementIndex); })
|
||||
await Vue.nextTick(() => {
|
||||
this.carouselInstance.to(clickedElementIndex);
|
||||
});
|
||||
this.selected = news;
|
||||
},
|
||||
contentURI: function (content_id) {
|
||||
return (
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
|
||||
"/CisVue/Cms/content/" +
|
||||
'/CisVue/Cms/content/' +
|
||||
content_id
|
||||
);
|
||||
},
|
||||
@@ -148,7 +168,7 @@ export default {
|
||||
return (
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
|
||||
"/CisVue/Cms/news"
|
||||
'/CisVue/Cms/news'
|
||||
);
|
||||
},
|
||||
setSingleNews(singleNews) {
|
||||
@@ -156,73 +176,72 @@ export default {
|
||||
this.$refs.newsModal.show();
|
||||
},
|
||||
initCarouselInstance() {
|
||||
Vue.nextTick(()=> {
|
||||
if(this.$refs.carousel) { // carousel ref might not exist in every widget width/height
|
||||
Vue.nextTick(() => {
|
||||
if (this.$refs.carousel) {
|
||||
// carousel ref might not exist in every widget width/height
|
||||
this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, {
|
||||
wrap: false, // keep this off even though it actually wraps
|
||||
interval: false
|
||||
interval: false,
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
initActiveItem() {
|
||||
Vue.nextTick(()=> {
|
||||
if (Array.isArray(this.$refs.carouselItems) && this.$refs.carouselItems.length >0) {
|
||||
this.$refs.carouselItems[0].classList.add("active")
|
||||
Vue.nextTick(() => {
|
||||
if (
|
||||
Array.isArray(this.$refs.carouselItems) &&
|
||||
this.$refs.carouselItems.length > 0
|
||||
) {
|
||||
this.$refs.carouselItems[0].classList.add('active');
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
width(newVal, oldVal) {
|
||||
if(oldVal == 1 && newVal > 1) { // carousel instance will have been disposed
|
||||
this.initCarouselInstance()
|
||||
this.initActiveItem()
|
||||
if (oldVal == 1 && newVal > 1) {
|
||||
// carousel instance will have been disposed
|
||||
this.initCarouselInstance();
|
||||
this.initActiveItem();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$emit("setConfig", false);
|
||||
this.$emit('setConfig', false);
|
||||
this.$api
|
||||
.call(ApiCms.news(MAX_LOADED_NEWS, MAX_NEW_AGE))
|
||||
.then(res => res.data)
|
||||
.call(ApiCms.newsForWidget(MAX_LOADED_NEWS, MAX_NEW_AGE))
|
||||
.then((res) => res.data)
|
||||
.then((news) => {
|
||||
this.allNewsList = Array.from(Object.values(news));
|
||||
this.allNewsList.sort((a, b) => {
|
||||
return new Date(b.datum) - new Date(a.datum);
|
||||
});
|
||||
this.selected = this.allNewsList.length ? this.allNewsList[0] : null
|
||||
this.initActiveItem()
|
||||
this.selected = this.allNewsList.length ? this.allNewsList[0] : null;
|
||||
this.initActiveItem();
|
||||
|
||||
this.updateNewsContentClasses();
|
||||
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("ERROR: ", err);
|
||||
console.error('ERROR: ', err);
|
||||
});
|
||||
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.container) {
|
||||
new ResizeObserver(entries => {
|
||||
new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
let w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
|
||||
let w = entry.contentBoxSize
|
||||
? entry.contentBoxSize[0].inlineSize
|
||||
: entry.contentRect.width;
|
||||
// TODO(chris): rework sizing
|
||||
if (w > 600)
|
||||
this.size = 3;
|
||||
else if (w > 350)
|
||||
this.size = 2;
|
||||
else if (w > 250)
|
||||
this.size = 1;
|
||||
else
|
||||
this.size = 0;
|
||||
}
|
||||
if (w > 600) this.size = 3;
|
||||
else if (w > 350) this.size = 2;
|
||||
else if (w > 250) this.size = 1;
|
||||
else this.size = 0;
|
||||
}
|
||||
}).observe(this.$refs.container);
|
||||
}
|
||||
|
||||
this.initCarouselInstance()
|
||||
|
||||
|
||||
this.initCarouselInstance();
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div ref="container" class="widgets-news h-100" :class="sizeClass" >
|
||||
|
||||
@@ -14,7 +14,8 @@ export default {
|
||||
emits: [
|
||||
'update:modelValue',
|
||||
'change',
|
||||
'changed'
|
||||
'changed',
|
||||
'tab-action'
|
||||
],
|
||||
props: {
|
||||
config: {
|
||||
@@ -234,6 +235,7 @@ export default {
|
||||
v-model="value"
|
||||
:config="tab.config"
|
||||
@update:suffix="updateSuffix($event)"
|
||||
@tab-action="$emit('tab-action', { key: tab.key, payload: $event })"
|
||||
></component>
|
||||
</keep-alive>
|
||||
</tabpanel>
|
||||
@@ -265,6 +267,7 @@ export default {
|
||||
v-model="value"
|
||||
:config="currentTab.config"
|
||||
@update:suffix="updateSuffix($event)"
|
||||
@tab-action="$emit('tab-action', { key: currentTab.key, payload: $event })"
|
||||
></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
@@ -272,4 +275,3 @@ export default {
|
||||
|
||||
</template>`
|
||||
};
|
||||
|
||||
|
||||
@@ -60359,6 +60359,46 @@ I have been informed that I am under no obligation to consent to the transmissio
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'ui',
|
||||
'phrase' => 'visible',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'sichtbar',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'visible',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'ui',
|
||||
'phrase' => 'publish',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'veroeffentlichen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'publish',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
// generic tabulator phrases END
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user