mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 15:04:04 +00:00
cms admin first draft, needs a lot of polishing still;
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
// Preferred organisational unit for a new content.
|
||||
// If the user is entitled for this unit, it wins.
|
||||
// If not, the first entitled unit wins.
|
||||
$config['default_oe_kurzbz'] = 'etw';
|
||||
@@ -75,6 +75,10 @@ $route['Abgabetool/Student'] = 'Cis/Abgabetool/Student';
|
||||
$route['Abgabetool/Student/(:any)'] = 'Cis/Abgabetool/Student/$1';
|
||||
$route['Abgabetool/Deadlines'] = 'Cis/Abgabetool/Deadlines';
|
||||
|
||||
// CMS Admin App Routes
|
||||
$route['cms'] = 'cms/index';
|
||||
$route['cms/.*'] = 'cms/index';
|
||||
|
||||
// Studierendenverwaltung App Routes
|
||||
$route['studvw'] = 'studentenverwaltung/index';
|
||||
$route['studvw/.*'] = 'studentenverwaltung/index';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
class Cms extends Auth_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(['index' => 'basis/cms:r']);
|
||||
$this->loadPhrases(['global', 'ui', 'cms']);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->load->view('Cms.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
<?php
|
||||
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
class CmsAdmin extends FHCAPI_Controller
|
||||
{
|
||||
const MAX_TREE_DEPTH = 50;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'getTree' => ['basis/cms:r'],
|
||||
'getContent' => ['basis/cms:r'],
|
||||
'getContentsprache' => ['basis/cms:r'],
|
||||
'getTemplates' => ['basis/cms:r'],
|
||||
'getOrganisationseinheiten' => ['basis/cms:r'],
|
||||
'getSprachen' => ['basis/cms:r'],
|
||||
'getUsage' => ['basis/cms:r'],
|
||||
'postContent' => ['basis/cms:rw'],
|
||||
'postTranslation' => ['basis/cms:rw'],
|
||||
'postVersion' => ['basis/cms:rw'],
|
||||
'putProperties' => ['basis/cms:rw'],
|
||||
'deleteContent' => ['basis/cms:rw'],
|
||||
'deleteContentsprache' => ['basis/cms:rw']
|
||||
]);
|
||||
|
||||
$this->load->library('CmsAdminLib');
|
||||
$this->load->library('PermissionLib');
|
||||
$this->load->model('content/Content_model', 'ContentModel');
|
||||
$this->load->model('content/Contentsprache_model', 'ContentspracheModel');
|
||||
$this->load->model('content/Contentchild_model', 'ContentchildModel');
|
||||
$this->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
|
||||
$this->load->model('content/Template_model', 'TemplateModel');
|
||||
|
||||
$this->loadPhrases(['global', 'cms']);
|
||||
}
|
||||
|
||||
public function getTree()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('menu', 'Menu', 'required|in_list[content,news]');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$menu = $this->input->get('menu', TRUE);
|
||||
$filter = $this->input->get('filter', TRUE);
|
||||
|
||||
$entitledResult = $this->cmsadminlib->getEntitledOe();
|
||||
$entitledOe = isError($entitledResult) ? [] : getData($entitledResult);
|
||||
|
||||
$nodes = [];
|
||||
|
||||
if ($menu === 'news')
|
||||
{
|
||||
$newsResult = $this->ContentModel->getNewsContent();
|
||||
if (!isError($newsResult) && getData($newsResult))
|
||||
{
|
||||
foreach (getData($newsResult) as $row)
|
||||
{
|
||||
$titel = '';
|
||||
$csResult = $this->ContentspracheModel->getOne(
|
||||
(int) $row->content_id, DEFAULT_LANGUAGE
|
||||
);
|
||||
if (!isError($csResult))
|
||||
$titel = getData($csResult)->titel;
|
||||
|
||||
$node = new stdClass();
|
||||
$node->content_id = (int) $row->content_id;
|
||||
$node->titel = mb_substr($titel, 0, 15) . ' '
|
||||
. date('d.m.Y', strtotime($row->insertamum));
|
||||
$node->template_kurzbz = $row->template_kurzbz;
|
||||
$node->oe_kurzbz = $row->oe_kurzbz;
|
||||
$node->aktiv = ($row->aktiv === 't' || $row->aktiv === true);
|
||||
$node->entitled = false;
|
||||
$node->groups = [];
|
||||
$node->children = [];
|
||||
$nodes[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!empty($filter))
|
||||
{
|
||||
$clean = str_replace(['!', '.', '?', ','], '', $filter);
|
||||
$searchItems = preg_split('/\s+/', $clean, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$rootsResult = $this->ContentModel->searchCms($searchItems);
|
||||
}
|
||||
else
|
||||
{
|
||||
$rootsResult = $this->ContentModel->getRootContent();
|
||||
}
|
||||
|
||||
if (!isError($rootsResult) && getData($rootsResult))
|
||||
{
|
||||
$visited = [];
|
||||
foreach (getData($rootsResult) as $row)
|
||||
{
|
||||
$node = $this->buildTreeNode(
|
||||
(int) $row->content_id, 0, $visited
|
||||
);
|
||||
if ($node !== null)
|
||||
$nodes[] = $node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$allIds = [];
|
||||
$this->collectContentIds($nodes, $allIds);
|
||||
|
||||
$groupMap = [];
|
||||
if (!empty($allIds))
|
||||
{
|
||||
$gResult = $this->ContentgruppeModel->getGruppenForContents($allIds);
|
||||
if (!isError($gResult))
|
||||
$groupMap = getData($gResult);
|
||||
}
|
||||
|
||||
$this->applyMeta($nodes, $entitledOe, $groupMap);
|
||||
|
||||
$result = [];
|
||||
foreach ($nodes as $node)
|
||||
{
|
||||
$pruned = $this->pruneUnentitled($node);
|
||||
if ($pruned !== null)
|
||||
$result[] = $pruned;
|
||||
}
|
||||
|
||||
$this->terminateWithSuccess($result);
|
||||
}
|
||||
|
||||
public function getContent()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
|
||||
$entitledResult = $this->cmsadminlib->isEntitledForContent($content_id);
|
||||
$entitled = $this->getDataOrTerminateWithError($entitledResult);
|
||||
|
||||
if (!$entitled)
|
||||
$this->terminateWithError('cms/keineBerechtigungFuerDiesenEintrag');
|
||||
|
||||
$this->ContentModel->resetQuery();
|
||||
$contentResult = $this->ContentModel->load($content_id);
|
||||
$contentData = $this->getDataOrTerminateWithError($contentResult);
|
||||
if (empty($contentData))
|
||||
$this->terminateWithError('cms/contentNichtGefunden');
|
||||
$content = $contentData[0];
|
||||
|
||||
$langResult = $this->ContentspracheModel->getLanguages($content_id);
|
||||
$languages = $this->getDataOrTerminateWithError($langResult);
|
||||
|
||||
$versions = new stdClass();
|
||||
foreach ($languages as $sprache)
|
||||
{
|
||||
$vResult = $this->ContentspracheModel->getVersions($content_id, $sprache);
|
||||
if (!isError($vResult) && getData($vResult))
|
||||
{
|
||||
$versions->$sprache = array_map(function ($v) {
|
||||
return (int) $v->version;
|
||||
}, getData($vResult));
|
||||
}
|
||||
}
|
||||
|
||||
$result = new stdClass();
|
||||
$result->content_id = (int) $content->content_id;
|
||||
$result->template_kurzbz = $content->template_kurzbz;
|
||||
$result->oe_kurzbz = $content->oe_kurzbz;
|
||||
$result->aktiv = ($content->aktiv === 't' || $content->aktiv === true);
|
||||
$result->menu_open = ($content->menu_open === 't' || $content->menu_open === true);
|
||||
$result->beschreibung = $content->beschreibung ?: '';
|
||||
$result->entitled = true;
|
||||
$result->languages = $languages;
|
||||
$result->versions = $versions;
|
||||
|
||||
$this->terminateWithSuccess($result);
|
||||
}
|
||||
|
||||
public function getContentsprache()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
$sprache = $this->input->get('sprache', TRUE);
|
||||
$version = $this->input->get('version', TRUE);
|
||||
|
||||
$result = $this->ContentspracheModel->getOne($content_id, $sprache, $version);
|
||||
if (isError($result))
|
||||
$this->terminateWithError('cms/versionNichtGefunden');
|
||||
|
||||
$data = getData($result);
|
||||
$data->sichtbar = ($data->sichtbar === 't' || $data->sichtbar === true);
|
||||
|
||||
$this->terminateWithSuccess($data);
|
||||
}
|
||||
|
||||
public function getTemplates()
|
||||
{
|
||||
$this->TemplateModel->addSelect('template_kurzbz, bezeichnung');
|
||||
$this->TemplateModel->addOrder('bezeichnung');
|
||||
$result = $this->TemplateModel->loadWhere([]);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function getOrganisationseinheiten()
|
||||
{
|
||||
$this->load->model(
|
||||
'organisation/Organisationseinheit_model', 'OrganisationseinheitModel'
|
||||
);
|
||||
$this->OrganisationseinheitModel->addSelect(
|
||||
'oe_kurzbz, bezeichnung, organisationseinheittyp_kurzbz, aktiv'
|
||||
);
|
||||
$this->OrganisationseinheitModel->addOrder('bezeichnung');
|
||||
$result = $this->OrganisationseinheitModel->loadWhere([]);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function getSprachen()
|
||||
{
|
||||
$this->load->model('system/Sprache_model', 'SpracheModel');
|
||||
$this->SpracheModel->addSelect('sprache, bezeichnung');
|
||||
$result = $this->SpracheModel->loadWhere([]);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function getUsage()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
$result = $this->ContentModel->getUsage($content_id);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function postContent()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('parent_content_id', 'Parent', 'is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$parent = $this->input->post('parent_content_id');
|
||||
if (empty($parent))
|
||||
$parent = null;
|
||||
|
||||
$result = $this->cmsadminlib->createContent($parent);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function postTranslation()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
|
||||
$this->form_validation->set_rules('target_sprache', 'Target Sprache', 'required');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$result = $this->cmsadminlib->createTranslation(
|
||||
$this->input->post('content_id'),
|
||||
$this->input->post('sprache'),
|
||||
$this->input->post('version'),
|
||||
$this->input->post('target_sprache')
|
||||
);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function postVersion()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'i'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$result = $this->cmsadminlib->createVersion(
|
||||
$this->input->post('content_id'),
|
||||
$this->input->post('sprache')
|
||||
);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function putProperties()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
|
||||
$this->form_validation->set_rules('template_kurzbz', 'Template', 'required');
|
||||
$this->form_validation->set_rules('oe_kurzbz', 'OE', 'required');
|
||||
$this->form_validation->set_rules('titel', 'Titel', 'required|max_length[256]');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$daten = [
|
||||
'content_id' => $this->input->post('content_id'),
|
||||
'sprache' => $this->input->post('sprache'),
|
||||
'version' => $this->input->post('version'),
|
||||
'template_kurzbz' => $this->input->post('template_kurzbz'),
|
||||
'oe_kurzbz' => $this->input->post('oe_kurzbz'),
|
||||
'titel' => $this->input->post('titel'),
|
||||
'beschreibung' => $this->input->post('beschreibung') ?: '',
|
||||
'aktiv' => filter_var($this->input->post('aktiv'), FILTER_VALIDATE_BOOLEAN),
|
||||
'menu_open' => filter_var($this->input->post('menu_open'), FILTER_VALIDATE_BOOLEAN),
|
||||
'sichtbar' => filter_var($this->input->post('sichtbar'), FILTER_VALIDATE_BOOLEAN)
|
||||
];
|
||||
|
||||
$result = $this->cmsadminlib->saveProperties($daten);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function deleteContent()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'd'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$result = $this->cmsadminlib->deleteContent($this->input->post('content_id'));
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function deleteContentsprache()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'd'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->validateContentSpracheVersion();
|
||||
|
||||
$result = $this->cmsadminlib->deleteVersion(
|
||||
$this->input->post('content_id'),
|
||||
$this->input->post('sprache'),
|
||||
$this->input->post('version')
|
||||
);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
private function validateContentSpracheVersion()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
}
|
||||
|
||||
private function buildTreeNode($content_id, $depth, &$visited)
|
||||
{
|
||||
if ($depth > self::MAX_TREE_DEPTH || isset($visited[$content_id]))
|
||||
return null;
|
||||
|
||||
$visited[$content_id] = true;
|
||||
|
||||
$this->ContentModel->resetQuery();
|
||||
$contentResult = $this->ContentModel->load($content_id);
|
||||
if (isError($contentResult) || empty(getData($contentResult)))
|
||||
return null;
|
||||
$content = getData($contentResult)[0];
|
||||
|
||||
$titel = '';
|
||||
$csResult = $this->ContentspracheModel->getOne($content_id, DEFAULT_LANGUAGE);
|
||||
if (!isError($csResult))
|
||||
$titel = getData($csResult)->titel;
|
||||
|
||||
$childrenResult = $this->ContentchildModel->getChilds($content_id, DEFAULT_LANGUAGE);
|
||||
$children = [];
|
||||
if (!isError($childrenResult) && getData($childrenResult))
|
||||
{
|
||||
foreach (getData($childrenResult) as $child)
|
||||
{
|
||||
$childNode = $this->buildTreeNode(
|
||||
(int) $child->child_content_id, $depth + 1, $visited
|
||||
);
|
||||
if ($childNode !== null)
|
||||
$children[] = $childNode;
|
||||
}
|
||||
}
|
||||
|
||||
$node = new stdClass();
|
||||
$node->content_id = (int) $content_id;
|
||||
$node->titel = $titel;
|
||||
$node->template_kurzbz = $content->template_kurzbz;
|
||||
$node->oe_kurzbz = $content->oe_kurzbz;
|
||||
$node->aktiv = ($content->aktiv === 't' || $content->aktiv === true);
|
||||
$node->entitled = false;
|
||||
$node->groups = [];
|
||||
$node->children = $children;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
private function collectContentIds($nodes, &$ids)
|
||||
{
|
||||
foreach ($nodes as $node)
|
||||
{
|
||||
$ids[] = $node->content_id;
|
||||
if (!empty($node->children))
|
||||
$this->collectContentIds($node->children, $ids);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyMeta(&$nodes, $entitledOe, $groupMap)
|
||||
{
|
||||
foreach ($nodes as $node)
|
||||
{
|
||||
$node->entitled = in_array($node->oe_kurzbz, $entitledOe);
|
||||
$node->groups = isset($groupMap[$node->content_id])
|
||||
? $groupMap[$node->content_id]
|
||||
: [];
|
||||
if (!empty($node->children))
|
||||
$this->applyMeta($node->children, $entitledOe, $groupMap);
|
||||
}
|
||||
}
|
||||
|
||||
private function pruneUnentitled($node)
|
||||
{
|
||||
$kept = [];
|
||||
foreach ($node->children as $child)
|
||||
{
|
||||
$pruned = $this->pruneUnentitled($child);
|
||||
if ($pruned !== null)
|
||||
$kept[] = $pruned;
|
||||
}
|
||||
$node->children = $kept;
|
||||
|
||||
if ($node->entitled || !empty($node->children))
|
||||
return $node;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
class CmsAdminInhalt extends FHCAPI_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'getFormSchema' => ['basis/cms:r'],
|
||||
'getFormData' => ['basis/cms:r'],
|
||||
'getLock' => ['basis/cms:r'],
|
||||
'getVersions' => ['basis/cms:r'],
|
||||
'putFormData' => ['basis/cms:rw'],
|
||||
'postLock' => ['basis/cms:rw'],
|
||||
'deleteLock' => ['basis/cms:rw'],
|
||||
'deleteLockForced' => ['basis/cms:rw']
|
||||
]);
|
||||
|
||||
$this->load->library('CmsAdminLib');
|
||||
$this->load->library('XsdSchemaLib');
|
||||
$this->load->library('PermissionLib');
|
||||
$this->load->model('content/Contentsprache_model', 'ContentspracheModel');
|
||||
$this->load->model('content/Template_model', 'TemplateModel');
|
||||
|
||||
$this->loadPhrases(['global', 'cms']);
|
||||
}
|
||||
|
||||
public function getFormSchema()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('template_kurzbz', 'Template', 'required');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$template_kurzbz = $this->input->get('template_kurzbz', TRUE);
|
||||
|
||||
$this->TemplateModel->resetQuery();
|
||||
$templateResult = $this->TemplateModel->load($template_kurzbz);
|
||||
$templateData = $this->getDataOrTerminateWithError($templateResult);
|
||||
if (empty($templateData))
|
||||
$this->terminateWithError('cms/vorlageNichtGefunden');
|
||||
|
||||
$schemaResult = $this->xsdschemalib->parseSchema(
|
||||
$templateData[0]->xsd, $template_kurzbz
|
||||
);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($schemaResult));
|
||||
}
|
||||
|
||||
public function getFormData()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
$sprache = $this->input->get('sprache', TRUE);
|
||||
$version = $this->input->get('version', TRUE);
|
||||
|
||||
$versionResult = $this->ContentspracheModel->getOne($content_id, $sprache, $version);
|
||||
$row = $this->getDataOrTerminateWithError($versionResult);
|
||||
|
||||
$this->load->model('content/Content_model', 'ContentModel');
|
||||
$this->ContentModel->resetQuery();
|
||||
$contentResult = $this->ContentModel->load($content_id);
|
||||
$contentData = $this->getDataOrTerminateWithError($contentResult);
|
||||
if (empty($contentData))
|
||||
$this->terminateWithError('cms/contentNichtGefunden');
|
||||
|
||||
$this->TemplateModel->resetQuery();
|
||||
$templateResult = $this->TemplateModel->load($contentData[0]->template_kurzbz);
|
||||
$templateData = $this->getDataOrTerminateWithError($templateResult);
|
||||
if (empty($templateData))
|
||||
$this->terminateWithError('cms/vorlageNichtGefunden');
|
||||
|
||||
$schemaResult = $this->xsdschemalib->parseSchema(
|
||||
$templateData[0]->xsd, $contentData[0]->template_kurzbz
|
||||
);
|
||||
$schema = $this->getDataOrTerminateWithError($schemaResult);
|
||||
|
||||
$valuesResult = $this->xsdschemalib->extractValues($row->content, $schema);
|
||||
$values = $this->getDataOrTerminateWithError($valuesResult);
|
||||
|
||||
$lockResult = $this->cmsadminlib->getLockState($content_id, $sprache, $version);
|
||||
$lock = $this->getDataOrTerminateWithError($lockResult);
|
||||
|
||||
$this->terminateWithSuccess([
|
||||
'schema' => $schema,
|
||||
'values' => $values,
|
||||
'sperre' => $lock
|
||||
]);
|
||||
}
|
||||
|
||||
public function getLock()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
$sprache = $this->input->get('sprache', TRUE);
|
||||
$version = $this->input->get('version', TRUE);
|
||||
|
||||
$result = $this->cmsadminlib->getLockState($content_id, $sprache, $version);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function getVersions()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
$sprache = $this->input->get('sprache', TRUE);
|
||||
|
||||
$result = $this->ContentspracheModel->getVersions($content_id, $sprache);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function putFormData()
|
||||
{
|
||||
// LEGACY-QUIRK: the legacy XSDFormPrinter_XML branch in admin.php checks neither the
|
||||
// permission type nor the lock. Kept as a functional copy. See Q3 in the contract.
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('sprache', 'Sprache', 'required');
|
||||
$this->form_validation->set_rules('version', 'Version', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->post('content_id');
|
||||
$sprache = $this->input->post('sprache');
|
||||
$version = $this->input->post('version');
|
||||
$values = $this->input->post('values');
|
||||
|
||||
if (is_string($values))
|
||||
$values = json_decode($values, true);
|
||||
|
||||
if (!is_array($values))
|
||||
$values = [];
|
||||
|
||||
$result = $this->cmsadminlib->saveContentXml($content_id, $sprache, $version, $values);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function postLock()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
|
||||
$this->terminateWithError('cms/keineBerechtigungFuerDieseAktion');
|
||||
|
||||
$contentsprache_id = $this->input->post('contentsprache_id');
|
||||
|
||||
$result = $this->cmsadminlib->lock($contentsprache_id);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function deleteLock()
|
||||
{
|
||||
// LEGACY-QUIRK: releaseOwnLocks releases all locks of the user. This method ignores
|
||||
// the given contentsprache_id on purpose. The parameter stays in the API contract, so
|
||||
// that a later fix does not change the interface. See Q1 in the contract.
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
|
||||
$this->terminateWithError('cms/keineBerechtigungFuerDieseAktion');
|
||||
|
||||
$result = $this->cmsadminlib->releaseOwnLocks();
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function deleteLockForced()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms_sperrfreigabe', 'su'))
|
||||
$this->terminateWithError('cms/keineBerechtigungFuerDieseAktion');
|
||||
|
||||
$contentsprache_id = $this->input->post('contentsprache_id');
|
||||
|
||||
$result = $this->cmsadminlib->forceRelease($contentsprache_id);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
class CmsAdminStruktur extends FHCAPI_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'getGruppen' => ['basis/cms:r'],
|
||||
'getAllGruppen' => ['basis/cms:r'],
|
||||
'getChilds' => ['basis/cms:r'],
|
||||
'getPossibleChilds' => ['basis/cms:r'],
|
||||
'postGruppe' => ['basis/cms:rw'],
|
||||
'deleteGruppe' => ['basis/cms:rw'],
|
||||
'postChild' => ['basis/cms:rw'],
|
||||
'deleteChild' => ['basis/cms:rw'],
|
||||
'putChildSort' => ['basis/cms:rw']
|
||||
]);
|
||||
|
||||
$this->load->library('PermissionLib');
|
||||
$this->load->model('content/Content_model', 'ContentModel');
|
||||
$this->load->model('content/Contentchild_model', 'ContentchildModel');
|
||||
$this->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
|
||||
|
||||
$this->loadPhrases(['global', 'cms']);
|
||||
}
|
||||
|
||||
public function getGruppen()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
|
||||
$result = $this->ContentgruppeModel->getGruppen($content_id);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function getAllGruppen()
|
||||
{
|
||||
$result = $this->ContentgruppeModel->getAllGruppen();
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function getChilds()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
$sprache = $this->input->get('sprache', TRUE);
|
||||
|
||||
if (empty($sprache))
|
||||
$sprache = DEFAULT_LANGUAGE;
|
||||
|
||||
$result = $this->ContentchildModel->getChilds($content_id, $sprache);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function getPossibleChilds()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_GET);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->get('content_id', TRUE);
|
||||
$sprache = $this->input->get('sprache', TRUE);
|
||||
|
||||
if (empty($sprache))
|
||||
$sprache = DEFAULT_LANGUAGE;
|
||||
|
||||
$result = $this->ContentModel->getPossibleChilds($content_id, $sprache);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
|
||||
public function postGruppe()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('gruppe_kurzbz', 'Gruppe', 'required');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$result = $this->ContentgruppeModel->insert([
|
||||
'content_id' => $this->input->post('content_id'),
|
||||
'gruppe_kurzbz' => $this->input->post('gruppe_kurzbz'),
|
||||
'insertamum' => date('Y-m-d H:i:s'),
|
||||
'insertvon' => getAuthUID()
|
||||
]);
|
||||
|
||||
if (isError($result))
|
||||
{
|
||||
$this->terminateWithError('cms/gruppeBereitsZugeordnet');
|
||||
}
|
||||
|
||||
$this->terminateWithSuccess(true);
|
||||
}
|
||||
|
||||
public function deleteGruppe()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('gruppe_kurzbz', 'Gruppe', 'required');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$result = $this->ContentgruppeModel->delete([
|
||||
$this->input->post('gruppe_kurzbz'),
|
||||
$this->input->post('content_id')
|
||||
]);
|
||||
$this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess(true);
|
||||
}
|
||||
|
||||
public function postChild()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('content_id', 'Content ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('child_content_id', 'Child Content ID', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$content_id = $this->input->post('content_id');
|
||||
|
||||
$sortResult = $this->ContentchildModel->getMaxSort($content_id);
|
||||
$sort = $this->getDataOrTerminateWithError($sortResult) + 1;
|
||||
|
||||
// LEGACY-QUIRK: tbl_contentchild has no unique index on (content_id,
|
||||
// child_content_id). The legacy code does not check for a duplicate, so a child can be
|
||||
// attached more than once. Kept as a functional copy. See section 3 of the contract.
|
||||
$result = $this->ContentchildModel->insert([
|
||||
'content_id' => $content_id,
|
||||
'child_content_id' => $this->input->post('child_content_id'),
|
||||
'sort' => $sort,
|
||||
'insertamum' => date('Y-m-d H:i:s'),
|
||||
'insertvon' => getAuthUID()
|
||||
]);
|
||||
$this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess(true);
|
||||
}
|
||||
|
||||
public function deleteChild()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('contentchild_id', 'Contentchild ID', 'required|is_natural');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$result = $this->ContentchildModel->delete($this->input->post('contentchild_id'));
|
||||
$this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess(true);
|
||||
}
|
||||
|
||||
public function putChildSort()
|
||||
{
|
||||
if (!$this->permissionlib->isBerechtigt('basis/cms', 'u'))
|
||||
$this->terminateWithError('cms/keineBerechtigung');
|
||||
|
||||
$this->load->library('form_validation');
|
||||
$this->form_validation->set_data($_POST);
|
||||
$this->form_validation->set_rules('contentchild_id', 'Contentchild ID', 'required|is_natural');
|
||||
$this->form_validation->set_rules('direction', 'Direction', 'required|in_list[up,down]');
|
||||
if ($this->form_validation->run() == FALSE)
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$result = $this->ContentchildModel->swapSort(
|
||||
$this->input->post('contentchild_id'),
|
||||
$this->input->post('direction')
|
||||
);
|
||||
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
<?php
|
||||
|
||||
if (!defined('BASEPATH'))
|
||||
exit('No direct script access allowed');
|
||||
|
||||
class CmsAdminLib
|
||||
{
|
||||
private $ci;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ci =& get_instance();
|
||||
|
||||
$this->ci->load->model('content/Content_model', 'ContentModel');
|
||||
$this->ci->load->model('content/Contentsprache_model', 'ContentspracheModel');
|
||||
$this->ci->load->model('content/Contentchild_model', 'ContentchildModel');
|
||||
$this->ci->load->model('content/Contentgruppe_model', 'ContentgruppeModel');
|
||||
$this->ci->load->model('content/Contentlog_model', 'ContentlogModel');
|
||||
$this->ci->load->model('content/Template_model', 'TemplateModel');
|
||||
|
||||
$this->ci->load->library('XsdSchemaLib');
|
||||
$this->ci->load->library('PermissionLib');
|
||||
$this->ci->config->load('cms');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object success(array) entitled oe_kurzbz values
|
||||
*/
|
||||
public function getEntitledOe()
|
||||
{
|
||||
$oe = $this->ci->permissionlib->getOE_isEntitledFor('basis/cms');
|
||||
|
||||
if ($oe === false || !is_array($oe))
|
||||
return success([]);
|
||||
|
||||
return success($oe);
|
||||
}
|
||||
|
||||
// LEGACY-QUIRK: the legacy code checks the organisational unit only when it loads the
|
||||
// page. It does not check on each write. This method exists, but it is called only
|
||||
// where the legacy code calls it. See Q7 in the contract.
|
||||
/**
|
||||
* @param int $content_id
|
||||
* @return object success(bool)
|
||||
*/
|
||||
public function isEntitledForContent($content_id)
|
||||
{
|
||||
$oeResult = $this->ci->ContentModel->getOeKurzbz($content_id);
|
||||
if (isError($oeResult))
|
||||
return $oeResult;
|
||||
|
||||
$entitledResult = $this->getEntitledOe();
|
||||
if (isError($entitledResult))
|
||||
return $entitledResult;
|
||||
|
||||
return success(in_array(getData($oeResult), getData($entitledResult)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object success(string) or error
|
||||
*/
|
||||
public function getDefaultOe()
|
||||
{
|
||||
$default = $this->ci->config->item('default_oe_kurzbz');
|
||||
|
||||
$entitledResult = $this->getEntitledOe();
|
||||
if (isError($entitledResult))
|
||||
return $entitledResult;
|
||||
$entitled = getData($entitledResult);
|
||||
|
||||
if (empty($entitled))
|
||||
return error('cms/keineBerechtigteOe');
|
||||
|
||||
if (in_array($default, $entitled))
|
||||
return success($default);
|
||||
|
||||
return success($entitled[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $parent_content_id
|
||||
* @return object success(array) with content_id, sprache, version
|
||||
*/
|
||||
public function createContent($parent_content_id = null)
|
||||
{
|
||||
$templatesResult = $this->ci->TemplateModel->loadWhere([]);
|
||||
if (isError($templatesResult))
|
||||
return $templatesResult;
|
||||
$templates = getData($templatesResult);
|
||||
if (empty($templates))
|
||||
return error('cms/keineVorlageVorhanden');
|
||||
$template = $templates[0];
|
||||
|
||||
$oeResult = $this->getDefaultOe();
|
||||
if (isError($oeResult))
|
||||
return $oeResult;
|
||||
$oe = getData($oeResult);
|
||||
|
||||
$this->ci->load->library('PhrasesLib');
|
||||
$phraseResult = $this->ci->phraseslib->getPhrases(
|
||||
'cms', DEFAULT_LANGUAGE, 'neuerEintrag', null, null, 'no'
|
||||
);
|
||||
$titel = $phraseResult->retval[0]->text;
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$uid = getAuthUID();
|
||||
|
||||
$this->ci->db->trans_start();
|
||||
|
||||
$contentResult = $this->ci->ContentModel->insert([
|
||||
'template_kurzbz' => $template->template_kurzbz,
|
||||
'oe_kurzbz' => $oe,
|
||||
'aktiv' => true,
|
||||
'menu_open' => false,
|
||||
'beschreibung' => '',
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid
|
||||
]);
|
||||
if (isError($contentResult))
|
||||
return $contentResult;
|
||||
$content_id = getData($contentResult);
|
||||
|
||||
$spracheResult = $this->ci->ContentspracheModel->insert([
|
||||
'content_id' => $content_id,
|
||||
'sprache' => DEFAULT_LANGUAGE,
|
||||
'version' => 1,
|
||||
'sichtbar' => true,
|
||||
'titel' => $titel,
|
||||
'content' => '<?xml version="1.0" encoding="UTF-8" ?><content></content>',
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid
|
||||
]);
|
||||
if (isError($spracheResult))
|
||||
return $spracheResult;
|
||||
|
||||
if ($parent_content_id !== null)
|
||||
{
|
||||
$maxSortResult = $this->ci->ContentchildModel->getMaxSort($parent_content_id);
|
||||
if (isError($maxSortResult))
|
||||
return $maxSortResult;
|
||||
|
||||
$childResult = $this->ci->ContentchildModel->insert([
|
||||
'content_id' => $parent_content_id,
|
||||
'child_content_id' => $content_id,
|
||||
'sort' => getData($maxSortResult) + 1,
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid
|
||||
]);
|
||||
if (isError($childResult))
|
||||
return $childResult;
|
||||
}
|
||||
|
||||
$this->ci->db->trans_complete();
|
||||
|
||||
if ($this->ci->db->trans_status() === false)
|
||||
return error('cms/speichernFehlgeschlagen');
|
||||
|
||||
return success([
|
||||
'content_id' => $content_id,
|
||||
'sprache' => DEFAULT_LANGUAGE,
|
||||
'version' => 1
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $content_id
|
||||
* @param string $sprache source language
|
||||
* @param int $version source version
|
||||
* @param string $neueSprache target language
|
||||
* @return object success(array) with contentsprache_id
|
||||
*/
|
||||
public function createTranslation($content_id, $sprache, $version, $neueSprache)
|
||||
{
|
||||
$existsResult = $this->ci->ContentspracheModel->exists($content_id, $neueSprache);
|
||||
if (isError($existsResult))
|
||||
return $existsResult;
|
||||
if (getData($existsResult))
|
||||
return error('cms/uebersetzungExistiert');
|
||||
|
||||
$sourceResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
|
||||
if (isError($sourceResult))
|
||||
return $sourceResult;
|
||||
$source = getData($sourceResult);
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$uid = getAuthUID();
|
||||
|
||||
$insertResult = $this->ci->ContentspracheModel->insert([
|
||||
'content_id' => $content_id,
|
||||
'sprache' => $neueSprache,
|
||||
'version' => $source->version,
|
||||
'sichtbar' => true,
|
||||
'titel' => $source->titel,
|
||||
'content' => $source->content,
|
||||
// The review feature is gone from this admin, but the columns stay and the
|
||||
// legacy admin still writes them. A copied row must not inherit the review
|
||||
// stamp of its source, so we clear it here.
|
||||
'reviewvon' => null,
|
||||
'reviewamum' => null,
|
||||
'gesperrt_uid' => null,
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid,
|
||||
'updateamum' => $now,
|
||||
'updatevon' => $uid
|
||||
]);
|
||||
if (isError($insertResult))
|
||||
return $insertResult;
|
||||
|
||||
return success(['contentsprache_id' => getData($insertResult)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @return object success(array) with version
|
||||
*/
|
||||
public function createVersion($content_id, $sprache)
|
||||
{
|
||||
$sourceResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, null);
|
||||
if (isError($sourceResult))
|
||||
return $sourceResult;
|
||||
$source = getData($sourceResult);
|
||||
|
||||
$maxResult = $this->ci->ContentspracheModel->getMaxVersion($content_id, $sprache);
|
||||
if (isError($maxResult))
|
||||
return $maxResult;
|
||||
$newVersion = getData($maxResult) + 1;
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$uid = getAuthUID();
|
||||
|
||||
$insertResult = $this->ci->ContentspracheModel->insert([
|
||||
'content_id' => $content_id,
|
||||
'sprache' => $sprache,
|
||||
'version' => $newVersion,
|
||||
'sichtbar' => false,
|
||||
'titel' => $source->titel,
|
||||
'content' => $source->content,
|
||||
// The review feature is gone from this admin, but the columns stay and the
|
||||
// legacy admin still writes them. A copied row must not inherit the review
|
||||
// stamp of its source, so we clear it here.
|
||||
'reviewvon' => null,
|
||||
'reviewamum' => null,
|
||||
'gesperrt_uid' => null,
|
||||
'insertamum' => $now,
|
||||
'insertvon' => $uid,
|
||||
'updateamum' => $now,
|
||||
'updatevon' => $uid
|
||||
]);
|
||||
if (isError($insertResult))
|
||||
return $insertResult;
|
||||
|
||||
return success(['version' => $newVersion]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $daten fields from contract 7.7
|
||||
* @return object success(true) or error
|
||||
*/
|
||||
public function saveProperties($daten)
|
||||
{
|
||||
$versionResult = $this->ci->ContentspracheModel->getOne(
|
||||
$daten['content_id'], $daten['sprache'], $daten['version']
|
||||
);
|
||||
if (isError($versionResult))
|
||||
return $versionResult;
|
||||
$row = getData($versionResult);
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$uid = getAuthUID();
|
||||
|
||||
$this->ci->db->trans_start();
|
||||
|
||||
$this->ci->ContentModel->update($daten['content_id'], [
|
||||
'template_kurzbz' => $daten['template_kurzbz'],
|
||||
'oe_kurzbz' => $daten['oe_kurzbz'],
|
||||
'aktiv' => $daten['aktiv'],
|
||||
'menu_open' => $daten['menu_open'],
|
||||
'beschreibung' => $daten['beschreibung'],
|
||||
'updateamum' => $now,
|
||||
'updatevon' => $uid
|
||||
]);
|
||||
|
||||
// LEGACY-QUIRK: prefs_save sets updateamum and updatevon in tbl_content only.
|
||||
// It does not touch tbl_contentsprache. Kept as a functional copy.
|
||||
$this->ci->ContentspracheModel->update($row->contentsprache_id, [
|
||||
'titel' => $daten['titel'],
|
||||
'sichtbar' => $daten['sichtbar']
|
||||
]);
|
||||
|
||||
$this->ci->db->trans_complete();
|
||||
|
||||
if ($this->ci->db->trans_status() === false)
|
||||
return error('cms/speichernFehlgeschlagen');
|
||||
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// LEGACY-QUIRK: the legacy XSDFormPrinter_XML branch checks neither the permission
|
||||
// type nor the lock. The form appears only when the user holds the lock, but the POST
|
||||
// itself is unprotected. See Q3 in the contract.
|
||||
/**
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @param int $version
|
||||
* @param array $values field name => value
|
||||
* @return object success(true) or error
|
||||
*/
|
||||
public function saveContentXml($content_id, $sprache, $version, $values)
|
||||
{
|
||||
$versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
|
||||
if (isError($versionResult))
|
||||
return $versionResult;
|
||||
$row = getData($versionResult);
|
||||
|
||||
$contentResult = $this->ci->ContentModel->load($content_id);
|
||||
if (isError($contentResult))
|
||||
return $contentResult;
|
||||
$content = getData($contentResult);
|
||||
if (empty($content))
|
||||
return error('cms/contentNichtGefunden');
|
||||
|
||||
$templateResult = $this->ci->TemplateModel->load($content[0]->template_kurzbz);
|
||||
if (isError($templateResult))
|
||||
return $templateResult;
|
||||
$template = getData($templateResult);
|
||||
if (empty($template))
|
||||
return error('cms/keineVorlageVorhanden');
|
||||
|
||||
$schemaResult = $this->ci->xsdschemalib->parseSchema(
|
||||
$template[0]->xsd, $content[0]->template_kurzbz
|
||||
);
|
||||
if (isError($schemaResult))
|
||||
return $schemaResult;
|
||||
|
||||
$xmlResult = $this->ci->xsdschemalib->buildXml(getData($schemaResult), $values);
|
||||
if (isError($xmlResult))
|
||||
return $xmlResult;
|
||||
|
||||
$updateResult = $this->ci->ContentspracheModel->update(
|
||||
$row->contentsprache_id,
|
||||
['content' => getData($xmlResult)]
|
||||
);
|
||||
if (isError($updateResult))
|
||||
return $updateResult;
|
||||
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// LEGACY-QUIRK: content::sperren does not check for an existing lock.
|
||||
// A second user overwrites the first lock. See Q2 in the contract.
|
||||
/**
|
||||
* @param int $contentsprache_id
|
||||
* @return object success(true) or error
|
||||
*/
|
||||
public function lock($contentsprache_id)
|
||||
{
|
||||
$uid = getAuthUID();
|
||||
|
||||
$this->ci->db->trans_start();
|
||||
|
||||
$logResult = $this->ci->ContentlogModel->insert([
|
||||
'uid' => $uid,
|
||||
'contentsprache_id' => $contentsprache_id,
|
||||
'start' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
if (isError($logResult))
|
||||
return $logResult;
|
||||
|
||||
$updateResult = $this->ci->ContentspracheModel->update($contentsprache_id, [
|
||||
'gesperrt_uid' => $uid
|
||||
]);
|
||||
if (isError($updateResult))
|
||||
return $updateResult;
|
||||
|
||||
$this->ci->db->trans_complete();
|
||||
|
||||
if ($this->ci->db->trans_status() === false)
|
||||
return error('cms/speichernFehlgeschlagen');
|
||||
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// LEGACY-QUIRK: content::freigabeUser releases ALL locks of the user, not only the
|
||||
// lock on this page. Kept as a functional copy. See Q1 in the contract.
|
||||
/**
|
||||
* @return object success(true)
|
||||
*/
|
||||
public function releaseOwnLocks()
|
||||
{
|
||||
$uid = getAuthUID();
|
||||
|
||||
$this->ci->db->trans_start();
|
||||
|
||||
$this->ci->db->query(
|
||||
'UPDATE campus.tbl_contentlog SET ende = now() WHERE uid = ? AND ende IS NULL',
|
||||
[$uid]
|
||||
);
|
||||
|
||||
$this->ci->db->query(
|
||||
'UPDATE campus.tbl_contentsprache SET gesperrt_uid = NULL WHERE gesperrt_uid = ?',
|
||||
[$uid]
|
||||
);
|
||||
|
||||
$this->ci->db->trans_complete();
|
||||
|
||||
if ($this->ci->db->trans_status() === false)
|
||||
return error('cms/speichernFehlgeschlagen');
|
||||
|
||||
return success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $contentsprache_id
|
||||
* @return object success(true)
|
||||
*/
|
||||
public function forceRelease($contentsprache_id)
|
||||
{
|
||||
$this->ci->db->trans_start();
|
||||
|
||||
$this->ci->db->query(
|
||||
'UPDATE campus.tbl_contentlog SET ende = now() WHERE contentsprache_id = ? AND ende IS NULL',
|
||||
[$contentsprache_id]
|
||||
);
|
||||
|
||||
$this->ci->db->query(
|
||||
'UPDATE campus.tbl_contentsprache SET gesperrt_uid = NULL WHERE contentsprache_id = ?',
|
||||
[$contentsprache_id]
|
||||
);
|
||||
|
||||
$this->ci->db->trans_complete();
|
||||
|
||||
if ($this->ci->db->trans_status() === false)
|
||||
return error('cms/speichernFehlgeschlagen');
|
||||
|
||||
return success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @param int $version
|
||||
* @return object success(array) lock state per contract 7.8
|
||||
*/
|
||||
public function getLockState($content_id, $sprache, $version)
|
||||
{
|
||||
$versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
|
||||
if (isError($versionResult))
|
||||
return $versionResult;
|
||||
$row = getData($versionResult);
|
||||
|
||||
$lockResult = $this->ci->ContentlogModel->getOpenLock($row->contentsprache_id);
|
||||
$logEntry = (!isError($lockResult)) ? getData($lockResult) : null;
|
||||
|
||||
// LEGACY-QUIRK: admin.php inserts an empty string, releasing writes NULL. Both mean
|
||||
// free. The contract reports NULL.
|
||||
$gesperrt_uid = ($row->gesperrt_uid === '') ? null : $row->gesperrt_uid;
|
||||
|
||||
return success([
|
||||
'contentsprache_id' => (int) $row->contentsprache_id,
|
||||
'gesperrt_uid' => $gesperrt_uid,
|
||||
'start' => $logEntry ? $logEntry->start : null,
|
||||
'own' => ($gesperrt_uid !== null && $gesperrt_uid === getAuthUID()),
|
||||
'may_force' => $this->ci->permissionlib->isBerechtigt('basis/cms_sperrfreigabe', 'su')
|
||||
]);
|
||||
}
|
||||
|
||||
// DEVIATION: admin.php ignores the return value of deleteContent and tells the user
|
||||
// nothing. This code reports the error. See Q5 in the contract.
|
||||
/**
|
||||
* @param int $content_id
|
||||
* @return object success(true) or error
|
||||
*/
|
||||
public function deleteContent($content_id)
|
||||
{
|
||||
$this->ci->db->trans_start();
|
||||
|
||||
$this->ci->db->query(
|
||||
'DELETE FROM campus.tbl_contentchild WHERE content_id = ?',
|
||||
[$content_id]
|
||||
);
|
||||
|
||||
$this->ci->db->query(
|
||||
'DELETE FROM campus.tbl_contentchild WHERE child_content_id = ?',
|
||||
[$content_id]
|
||||
);
|
||||
|
||||
$this->ci->db->query(
|
||||
'DELETE FROM campus.tbl_contentlog WHERE contentsprache_id IN (SELECT contentsprache_id FROM campus.tbl_contentsprache WHERE content_id = ?)',
|
||||
[$content_id]
|
||||
);
|
||||
|
||||
$this->ci->db->query(
|
||||
'DELETE FROM campus.tbl_contentsprache WHERE content_id = ?',
|
||||
[$content_id]
|
||||
);
|
||||
|
||||
$this->ci->db->query(
|
||||
'DELETE FROM campus.tbl_contentgruppe WHERE content_id = ?',
|
||||
[$content_id]
|
||||
);
|
||||
|
||||
$this->ci->db->query(
|
||||
'DELETE FROM campus.tbl_content WHERE content_id = ?',
|
||||
[$content_id]
|
||||
);
|
||||
|
||||
$this->ci->db->trans_complete();
|
||||
|
||||
if ($this->ci->db->trans_status() === false)
|
||||
return error('cms/loeschenNichtMoeglichReferenzen');
|
||||
|
||||
return success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @param int $version
|
||||
* @return object success(true) or error
|
||||
*/
|
||||
public function deleteVersion($content_id, $sprache, $version)
|
||||
{
|
||||
if ($sprache === DEFAULT_LANGUAGE)
|
||||
{
|
||||
$countResult = $this->ci->ContentspracheModel->getNumberOfVersions($content_id, $sprache);
|
||||
if (isError($countResult))
|
||||
return $countResult;
|
||||
if (getData($countResult) === 1)
|
||||
return error('cms/letzteVersionNichtLoeschbar');
|
||||
}
|
||||
|
||||
$versionResult = $this->ci->ContentspracheModel->getOne($content_id, $sprache, $version);
|
||||
if (isError($versionResult))
|
||||
return $versionResult;
|
||||
$row = getData($versionResult);
|
||||
|
||||
$this->ci->db->trans_start();
|
||||
|
||||
$this->ci->db->query(
|
||||
'DELETE FROM campus.tbl_contentlog WHERE contentsprache_id = ?',
|
||||
[$row->contentsprache_id]
|
||||
);
|
||||
|
||||
$this->ci->db->query(
|
||||
'DELETE FROM campus.tbl_contentsprache WHERE content_id = ? AND sprache = ? AND version = ?',
|
||||
[$content_id, $sprache, $version]
|
||||
);
|
||||
|
||||
$this->ci->db->trans_complete();
|
||||
|
||||
if ($this->ci->db->trans_status() === false)
|
||||
return error('cms/loeschenFehlgeschlagen');
|
||||
|
||||
return success(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
if (!defined('BASEPATH'))
|
||||
exit('No direct script access allowed');
|
||||
|
||||
use \DOMDocument as DOMDocument;
|
||||
|
||||
class XsdSchemaLib
|
||||
{
|
||||
/**
|
||||
* Converts an XSD into a field schema.
|
||||
* @param string $xsd the XSD as a string
|
||||
* @param string $templateKurzbz short name of the template
|
||||
* @return object success(array) with template_kurzbz, root, fields
|
||||
*/
|
||||
public function parseSchema($xsd, $templateKurzbz)
|
||||
{
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
|
||||
if (!$dom->loadXML($xsd))
|
||||
{
|
||||
libxml_clear_errors();
|
||||
return error('cms/xsdUngueltig');
|
||||
}
|
||||
|
||||
libxml_clear_errors();
|
||||
|
||||
$root = null;
|
||||
$fields = array();
|
||||
$this->_walkElements($dom->documentElement, $root, $fields);
|
||||
|
||||
return success(array(
|
||||
'template_kurzbz' => $templateKurzbz,
|
||||
'root' => $root,
|
||||
'fields' => $fields
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the values of a content from the XML.
|
||||
* @param string $xml the value of tbl_contentsprache.content
|
||||
* @param array $schema the result of parseSchema
|
||||
* @return object success(array) field name => value
|
||||
*/
|
||||
public function extractValues($xml, $schema)
|
||||
{
|
||||
$values = array();
|
||||
|
||||
foreach ($schema['fields'] as $field)
|
||||
{
|
||||
$values[$field['name']] = '';
|
||||
}
|
||||
|
||||
if ($xml === null || trim($xml) === '')
|
||||
{
|
||||
return success($values);
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
|
||||
if (!$dom->loadXML($xml))
|
||||
{
|
||||
libxml_clear_errors();
|
||||
return success($values);
|
||||
}
|
||||
|
||||
libxml_clear_errors();
|
||||
|
||||
foreach ($schema['fields'] as $field)
|
||||
{
|
||||
$nodes = $dom->getElementsByTagName($field['name']);
|
||||
if ($nodes->length > 0)
|
||||
{
|
||||
$values[$field['name']] = $nodes->item(0)->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
return success($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the XML from the values.
|
||||
* @param array $schema the result of parseSchema
|
||||
* @param array $values field name => value
|
||||
* @return object success(string) the XML
|
||||
*/
|
||||
public function buildXml($schema, $values)
|
||||
{
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||
$xml .= "\n<" . $schema['root'] . '>';
|
||||
|
||||
foreach ($schema['fields'] as $field)
|
||||
{
|
||||
$value = isset($values[$field['name']]) ? $values[$field['name']] : '';
|
||||
$xml .= "\n<" . $field['name'] . '><![CDATA[' . $this->_escapeCdata($value) . ']]></' . $field['name'] . '>';
|
||||
}
|
||||
|
||||
$xml .= "\n</" . $schema['root'] . '>';
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$check = new DOMDocument();
|
||||
|
||||
if (!$check->loadXML($xml))
|
||||
{
|
||||
libxml_clear_errors();
|
||||
return error('cms/xmlUngueltig');
|
||||
}
|
||||
|
||||
libxml_clear_errors();
|
||||
|
||||
return success($xml);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private methods
|
||||
|
||||
/**
|
||||
* Walks xs:element nodes recursively.
|
||||
* The first element without a type attribute is the root.
|
||||
* Elements with a type attribute are fields.
|
||||
* xs:complexType and xs:sequence are traversed and ignored.
|
||||
*/
|
||||
private function _walkElements($node, &$root, &$fields)
|
||||
{
|
||||
if ($node->nodeType !== XML_ELEMENT_NODE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$localName = $node->localName;
|
||||
|
||||
if ($localName === 'element')
|
||||
{
|
||||
$typeAttr = $node->getAttribute('type');
|
||||
|
||||
if ($typeAttr === '')
|
||||
{
|
||||
if ($root === null)
|
||||
{
|
||||
$root = $node->getAttribute('name');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$minOccurs = $node->getAttribute('minOccurs');
|
||||
$required = ($minOccurs === '') ? true : ((int)$minOccurs > 0);
|
||||
|
||||
$fields[] = array(
|
||||
'name' => $node->getAttribute('name'),
|
||||
'type' => $this->_mapType($typeAttr),
|
||||
'required' => $required
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($node->childNodes as $child)
|
||||
{
|
||||
$this->_walkElements($child, $root, $fields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an XSD type to the field schema type.
|
||||
*/
|
||||
private function _mapType($xsdType)
|
||||
{
|
||||
$map = array(
|
||||
'xs:string' => 'string',
|
||||
'xs:decimal' => 'string',
|
||||
'xs:integer' => 'string',
|
||||
'xs:positiveInteger' => 'string',
|
||||
'xs:date' => 'date',
|
||||
'wysiwyg' => 'wysiwyg',
|
||||
'file' => 'file',
|
||||
'boolean' => 'boolean'
|
||||
);
|
||||
|
||||
return isset($map[$xsdType]) ? $map[$xsdType] : 'string';
|
||||
}
|
||||
|
||||
// Escapes ]]> in the value. The legacy code does not do this and writes broken XML.
|
||||
private function _escapeCdata($value)
|
||||
{
|
||||
return str_replace(']]>', ']]]]><![CDATA[>', $value);
|
||||
}
|
||||
}
|
||||
@@ -323,4 +323,209 @@ class Content_model extends DB_Model
|
||||
|
||||
return success(isset($result[$root_content_id]) ? $result[$root_content_id] : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root contents without a parent in tbl_contentchild. Excludes news.
|
||||
* @return stdClass success with array of rows or error
|
||||
*/
|
||||
public function getRootContent()
|
||||
{
|
||||
$query = '
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT DISTINCT ON (content_id) *
|
||||
FROM campus.tbl_content
|
||||
LEFT JOIN campus.tbl_contentchild USING (content_id)
|
||||
WHERE tbl_content.template_kurzbz <> ?
|
||||
AND content_id NOT IN (
|
||||
SELECT child_content_id FROM campus.tbl_contentchild
|
||||
WHERE child_content_id = tbl_content.content_id)
|
||||
) AS a
|
||||
ORDER BY sort, content_id
|
||||
';
|
||||
|
||||
return $this->execReadOnlyQuery($query, ['news']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recent news from the last two months, max 100 rows.
|
||||
* @return stdClass success with array of rows or error
|
||||
*/
|
||||
public function getNewsContent()
|
||||
{
|
||||
$query = "
|
||||
SELECT *
|
||||
FROM campus.tbl_content
|
||||
JOIN campus.tbl_news USING (content_id)
|
||||
WHERE tbl_news.datum >= now() - '2 month'::interval
|
||||
ORDER BY datum DESC
|
||||
LIMIT 100
|
||||
";
|
||||
|
||||
return $this->execReadOnlyQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search contents by content_id or titel. Excludes news. Returns content_ids only.
|
||||
* @param array $searchItems search terms
|
||||
* @return stdClass success with array of rows or error
|
||||
*/
|
||||
public function searchCms($searchItems)
|
||||
{
|
||||
if (empty($searchItems))
|
||||
return success([]);
|
||||
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
foreach ($searchItems as $term)
|
||||
{
|
||||
$conditions[] = '(content_id::text = ? OR lower(titel) LIKE lower(?))';
|
||||
$params[] = $term;
|
||||
$params[] = '%' . $term . '%';
|
||||
}
|
||||
|
||||
$query = '
|
||||
SELECT DISTINCT tbl_content.content_id
|
||||
FROM campus.tbl_contentsprache
|
||||
JOIN campus.tbl_content USING (content_id)
|
||||
WHERE tbl_content.template_kurzbz <> ?
|
||||
AND (' . implode(' OR ', $conditions) . ')
|
||||
ORDER BY content_id
|
||||
';
|
||||
|
||||
array_unshift($params, 'news');
|
||||
|
||||
return $this->execReadOnlyQuery($query, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* All contents eligible as children: excludes ancestors, self, and news.
|
||||
* @param int $content_id the content to find children for
|
||||
* @param string $sprache language for the titel subselect
|
||||
* @return stdClass success with array of rows or error
|
||||
*/
|
||||
public function getPossibleChilds($content_id, $sprache)
|
||||
{
|
||||
$query = '
|
||||
SELECT content_id, oe_kurzbz, template_kurzbz,
|
||||
(SELECT titel FROM campus.tbl_contentsprache
|
||||
WHERE sprache = ? AND content_id = tbl_content.content_id
|
||||
ORDER BY version LIMIT 1) AS titel
|
||||
FROM campus.tbl_content
|
||||
WHERE content_id NOT IN (
|
||||
WITH RECURSIVE parents(content_id, child_content_id) AS (
|
||||
SELECT content_id, child_content_id FROM campus.tbl_contentchild
|
||||
WHERE child_content_id = ?
|
||||
UNION ALL
|
||||
SELECT cc.content_id, cc.child_content_id
|
||||
FROM campus.tbl_contentchild cc, parents
|
||||
WHERE cc.child_content_id = parents.content_id)
|
||||
SELECT content_id FROM parents GROUP BY content_id)
|
||||
AND content_id <> ?
|
||||
AND template_kurzbz <> ?
|
||||
ORDER BY titel
|
||||
';
|
||||
|
||||
return $this->execReadOnlyQuery($query, [$sprache, $content_id, $content_id, 'news']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a content is referenced. Returns [{table, label}] for the delete dialog.
|
||||
* Tolerates missing addon/testtool schemas.
|
||||
* @param int $content_id
|
||||
* @return stdClass success with flat array or error
|
||||
*/
|
||||
public function getUsage($content_id)
|
||||
{
|
||||
$coreQuery = "
|
||||
SELECT 'campus.tbl_infoscreen_content' AS \"table\",
|
||||
infoscreen_id::text AS label
|
||||
FROM campus.tbl_infoscreen_content WHERE content_id = ?
|
||||
UNION ALL
|
||||
SELECT 'campus.tbl_news', betreff
|
||||
FROM campus.tbl_news WHERE content_id = ?
|
||||
UNION ALL
|
||||
SELECT 'public.tbl_ort', ort_kurzbz
|
||||
FROM public.tbl_ort WHERE content_id = ?
|
||||
UNION ALL
|
||||
SELECT 'public.tbl_service', bezeichnung
|
||||
FROM public.tbl_service WHERE content_id = ?
|
||||
UNION ALL
|
||||
SELECT 'public.tbl_statistik', bezeichnung
|
||||
FROM public.tbl_statistik WHERE content_id = ?
|
||||
";
|
||||
$coreParams = [$content_id, $content_id, $content_id, $content_id, $content_id];
|
||||
|
||||
$coreResult = $this->execReadOnlyQuery($coreQuery, $coreParams);
|
||||
$rows = [];
|
||||
|
||||
if (!isError($coreResult) && getData($coreResult))
|
||||
$rows = getData($coreResult);
|
||||
|
||||
// Optional schemas: testtool and addon may not exist. A failing query still writes a
|
||||
// db error into the response envelope, so check for the table before querying it.
|
||||
$optionalQueries = [
|
||||
[
|
||||
'testtool', 'tbl_ablauf_vorgaben',
|
||||
"SELECT 'testtool.tbl_ablauf_vorgaben' AS \"table\",
|
||||
ablauf_vorgaben_id::text AS label
|
||||
FROM testtool.tbl_ablauf_vorgaben WHERE content_id = ?"
|
||||
],
|
||||
[
|
||||
'addon', 'tbl_software',
|
||||
"SELECT 'addon.tbl_software' AS \"table\",
|
||||
software_id::text AS label
|
||||
FROM addon.tbl_software WHERE content_id = ?"
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($optionalQueries as $opt)
|
||||
{
|
||||
if (!$this->tableExists($opt[0], $opt[1]))
|
||||
continue;
|
||||
|
||||
$result = $this->execReadOnlyQuery($opt[2], [$content_id]);
|
||||
if (!isError($result) && getData($result))
|
||||
$rows = array_merge($rows, getData($result));
|
||||
}
|
||||
|
||||
return success($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if a base table exists. Guards the optional testtool and addon schemas.
|
||||
* @param string $schema
|
||||
* @param string $table
|
||||
* @return bool
|
||||
*/
|
||||
private function tableExists($schema, $table)
|
||||
{
|
||||
$query = "
|
||||
SELECT 1 AS exists
|
||||
FROM information_schema.tables
|
||||
WHERE table_catalog = ? AND table_schema = ? AND table_name = ?
|
||||
";
|
||||
|
||||
return hasData($this->execReadOnlyQuery($query, [DB_NAME, $schema, $table]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns oe_kurzbz for a content as a string.
|
||||
* @param int $content_id
|
||||
* @return stdClass success with string or error
|
||||
*/
|
||||
public function getOeKurzbz($content_id)
|
||||
{
|
||||
$query = 'SELECT oe_kurzbz FROM campus.tbl_content WHERE content_id = ?';
|
||||
$result = $this->execReadOnlyQuery($query, [$content_id]);
|
||||
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
$data = getData($result);
|
||||
if (empty($data))
|
||||
return error('Content not found');
|
||||
|
||||
return success($data[0]->oe_kurzbz);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,140 @@ class Contentchild_model extends DB_Model
|
||||
$this->dbTable = 'campus.tbl_contentchild';
|
||||
$this->pk = 'contentchild_id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Children with titel, sorted by sort.
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @return stdClass success with array of rows or error
|
||||
*/
|
||||
public function getChilds($content_id, $sprache)
|
||||
{
|
||||
$query = '
|
||||
SELECT cc.contentchild_id, cc.child_content_id, cc.sort,
|
||||
(SELECT titel FROM campus.tbl_contentsprache
|
||||
WHERE content_id = cc.child_content_id AND sprache = ?
|
||||
ORDER BY version LIMIT 1) AS titel
|
||||
FROM campus.tbl_contentchild cc
|
||||
WHERE cc.content_id = ?
|
||||
ORDER BY cc.sort, cc.contentchild_id
|
||||
';
|
||||
|
||||
return $this->execReadOnlyQuery($query, [$sprache, $content_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highest sort value. Returns 0 if no row exists.
|
||||
* @param int $content_id
|
||||
* @return stdClass success with int or error
|
||||
*/
|
||||
public function getMaxSort($content_id)
|
||||
{
|
||||
$query = '
|
||||
SELECT COALESCE(MAX(sort), 0) AS max_sort
|
||||
FROM campus.tbl_contentchild
|
||||
WHERE content_id = ?
|
||||
';
|
||||
|
||||
$result = $this->execReadOnlyQuery($query, [$content_id]);
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
return success((int) getData($result)[0]->max_sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* All descendant content_ids recursively. Returns a flat array.
|
||||
* @param int $content_id
|
||||
* @return stdClass success with array of ints or error
|
||||
*/
|
||||
public function getAllChildIds($content_id)
|
||||
{
|
||||
$query = '
|
||||
WITH RECURSIVE childs(content_id, child_content_id) AS (
|
||||
SELECT content_id, child_content_id FROM campus.tbl_contentchild WHERE content_id = ?
|
||||
UNION ALL
|
||||
SELECT cc.content_id, cc.child_content_id
|
||||
FROM campus.tbl_contentchild cc, childs
|
||||
WHERE cc.content_id = childs.child_content_id)
|
||||
SELECT DISTINCT child_content_id FROM childs
|
||||
';
|
||||
|
||||
$result = $this->execReadOnlyQuery($query, [$content_id]);
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
$ids = [];
|
||||
if (getData($result))
|
||||
{
|
||||
foreach (getData($result) as $row)
|
||||
$ids[] = (int) $row->child_content_id;
|
||||
}
|
||||
|
||||
return success($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap sort value with the neighbour. Direction is 'up' or 'down'.
|
||||
* @param int $contentchild_id
|
||||
* @param string $direction 'up' or 'down'
|
||||
* @return stdClass success or error
|
||||
*/
|
||||
public function swapSort($contentchild_id, $direction)
|
||||
{
|
||||
$currentResult = $this->load($contentchild_id);
|
||||
if (isError($currentResult))
|
||||
return $currentResult;
|
||||
$current = getData($currentResult)[0];
|
||||
|
||||
if ($direction === 'up')
|
||||
{
|
||||
$neighbourQuery = '
|
||||
SELECT contentchild_id, sort
|
||||
FROM campus.tbl_contentchild
|
||||
WHERE content_id = ? AND sort < ?
|
||||
ORDER BY sort DESC
|
||||
LIMIT 1
|
||||
';
|
||||
}
|
||||
else
|
||||
{
|
||||
$neighbourQuery = '
|
||||
SELECT contentchild_id, sort
|
||||
FROM campus.tbl_contentchild
|
||||
WHERE content_id = ? AND sort > ?
|
||||
ORDER BY sort ASC
|
||||
LIMIT 1
|
||||
';
|
||||
}
|
||||
|
||||
$neighbourResult = $this->execReadOnlyQuery($neighbourQuery, [$current->content_id, $current->sort]);
|
||||
if (isError($neighbourResult))
|
||||
return $neighbourResult;
|
||||
|
||||
$neighbourData = getData($neighbourResult);
|
||||
if (empty($neighbourData))
|
||||
return error($direction === 'up' ? 'cms/bereitsGanzOben' : 'cms/bereitsGanzUnten');
|
||||
|
||||
$neighbour = $neighbourData[0];
|
||||
|
||||
// Transaction wraps the swap; the legacy does not.
|
||||
$this->db->trans_start();
|
||||
|
||||
$this->execQuery(
|
||||
'UPDATE campus.tbl_contentchild SET sort = ? WHERE contentchild_id = ?',
|
||||
[$neighbour->sort, $current->contentchild_id]
|
||||
);
|
||||
$this->execQuery(
|
||||
'UPDATE campus.tbl_contentchild SET sort = ? WHERE contentchild_id = ?',
|
||||
[$current->sort, $neighbour->contentchild_id]
|
||||
);
|
||||
|
||||
$this->db->trans_complete();
|
||||
|
||||
if ($this->db->trans_status() === false)
|
||||
return error('Sort swap failed');
|
||||
|
||||
return success(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,5 +56,77 @@ class Contentgruppe_model extends DB_Model
|
||||
return $result;
|
||||
return success(!!getData($result));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Groups assigned to a content, with bezeichnung.
|
||||
* @param int $content_id
|
||||
* @return stdClass success with array of rows or error
|
||||
*/
|
||||
public function getGruppen($content_id)
|
||||
{
|
||||
$query = '
|
||||
SELECT cg.gruppe_kurzbz, g.bezeichnung
|
||||
FROM campus.tbl_contentgruppe cg
|
||||
JOIN public.tbl_gruppe g USING (gruppe_kurzbz)
|
||||
WHERE cg.content_id = ?
|
||||
ORDER BY cg.gruppe_kurzbz
|
||||
';
|
||||
|
||||
return $this->execReadOnlyQuery($query, [$content_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* All selectable groups. Returns gruppe_kurzbz and bezeichnung.
|
||||
* @return stdClass success with array of rows or error
|
||||
*/
|
||||
public function getAllGruppen()
|
||||
{
|
||||
// content_visible marks a group as selectable for content rights. Generated groups
|
||||
// carry FALSE. admin.php filters the same way via getgruppe(...,$content_visible=true).
|
||||
$query = '
|
||||
SELECT gruppe_kurzbz, bezeichnung
|
||||
FROM public.tbl_gruppe
|
||||
WHERE content_visible = TRUE
|
||||
ORDER BY gruppe_kurzbz
|
||||
';
|
||||
|
||||
return $this->execReadOnlyQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups for multiple contents in one query. Returns content_id => [gruppe_kurzbz, ...].
|
||||
* @param array $contentIds array of content_id values
|
||||
* @return stdClass success with associative array or error
|
||||
*/
|
||||
public function getGruppenForContents($contentIds)
|
||||
{
|
||||
if (empty($contentIds))
|
||||
return success([]);
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($contentIds), '?'));
|
||||
$query = '
|
||||
SELECT content_id, gruppe_kurzbz
|
||||
FROM campus.tbl_contentgruppe
|
||||
WHERE content_id IN (' . $placeholders . ')
|
||||
ORDER BY content_id, gruppe_kurzbz
|
||||
';
|
||||
|
||||
$result = $this->execReadOnlyQuery($query, array_values($contentIds));
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
$map = [];
|
||||
if (getData($result))
|
||||
{
|
||||
foreach (getData($result) as $row)
|
||||
{
|
||||
$id = (int) $row->content_id;
|
||||
if (!isset($map[$id]))
|
||||
$map[$id] = [];
|
||||
$map[$id][] = $row->gruppe_kurzbz;
|
||||
}
|
||||
}
|
||||
|
||||
return success($map);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,26 @@ class Contentlog_model extends DB_Model
|
||||
$this->dbTable = 'campus.tbl_contentlog';
|
||||
$this->pk = 'contentlog_id';
|
||||
}
|
||||
|
||||
/**
|
||||
* The open lock entry (ende IS NULL). Returns null if none exists.
|
||||
* @param int $contentsprache_id
|
||||
* @return stdClass success with row or null, or error
|
||||
*/
|
||||
public function getOpenLock($contentsprache_id)
|
||||
{
|
||||
$query = '
|
||||
SELECT contentlog_id, uid, contentsprache_id, start
|
||||
FROM campus.tbl_contentlog
|
||||
WHERE contentsprache_id = ? AND ende IS NULL
|
||||
LIMIT 1
|
||||
';
|
||||
|
||||
$result = $this->execReadOnlyQuery($query, [$contentsprache_id]);
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
$data = getData($result);
|
||||
return success(empty($data) ? null : $data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,5 +38,128 @@ class Contentsprache_model extends DB_Model
|
||||
|
||||
return success(!!getData($result));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* All versions of a language, ascending by version.
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @return stdClass success with array of rows or error
|
||||
*/
|
||||
public function getVersions($content_id, $sprache)
|
||||
{
|
||||
$query = '
|
||||
SELECT contentsprache_id, version, sichtbar, titel,
|
||||
insertamum, insertvon, updateamum, updatevon, gesperrt_uid
|
||||
FROM campus.tbl_contentsprache
|
||||
WHERE content_id = ? AND sprache = ?
|
||||
ORDER BY version ASC
|
||||
';
|
||||
|
||||
return $this->execReadOnlyQuery($query, [$content_id, $sprache]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Distinct languages for a content. Returns a flat array of strings.
|
||||
* @param int $content_id
|
||||
* @return stdClass success with array of strings or error
|
||||
*/
|
||||
public function getLanguages($content_id)
|
||||
{
|
||||
$query = '
|
||||
SELECT DISTINCT sprache
|
||||
FROM campus.tbl_contentsprache
|
||||
WHERE content_id = ?
|
||||
ORDER BY sprache
|
||||
';
|
||||
|
||||
$result = $this->execReadOnlyQuery($query, [$content_id]);
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
$languages = [];
|
||||
if (getData($result))
|
||||
{
|
||||
foreach (getData($result) as $row)
|
||||
$languages[] = $row->sprache;
|
||||
}
|
||||
|
||||
return success($languages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highest version number. Returns 0 if no row exists.
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @return stdClass success with int or error
|
||||
*/
|
||||
public function getMaxVersion($content_id, $sprache)
|
||||
{
|
||||
$query = '
|
||||
SELECT COALESCE(MAX(version), 0) AS max_version
|
||||
FROM campus.tbl_contentsprache
|
||||
WHERE content_id = ? AND sprache = ?
|
||||
';
|
||||
|
||||
$result = $this->execReadOnlyQuery($query, [$content_id, $sprache]);
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
return success((int) getData($result)[0]->max_version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Row count for a content+language. Used for the delete guard.
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @return stdClass success with int or error
|
||||
*/
|
||||
public function getNumberOfVersions($content_id, $sprache)
|
||||
{
|
||||
$query = '
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM campus.tbl_contentsprache
|
||||
WHERE content_id = ? AND sprache = ?
|
||||
';
|
||||
|
||||
$result = $this->execReadOnlyQuery($query, [$content_id, $sprache]);
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
return success((int) getData($result)[0]->cnt);
|
||||
}
|
||||
|
||||
/**
|
||||
* One version, all columns. If $version is empty, returns the highest version.
|
||||
* @param int $content_id
|
||||
* @param string $sprache
|
||||
* @param int|null $version
|
||||
* @return stdClass success with single row or error
|
||||
*/
|
||||
public function getOne($content_id, $sprache, $version = null)
|
||||
{
|
||||
$params = [$content_id, $sprache];
|
||||
|
||||
$versionClause = '';
|
||||
if (!empty($version))
|
||||
{
|
||||
$versionClause = ' AND version = ?';
|
||||
$params[] = $version;
|
||||
}
|
||||
|
||||
$query = '
|
||||
SELECT * FROM campus.tbl_contentsprache
|
||||
WHERE content_id = ? AND sprache = ?' . $versionClause . '
|
||||
ORDER BY version DESC LIMIT 1
|
||||
';
|
||||
|
||||
$result = $this->execReadOnlyQuery($query, $params);
|
||||
if (isError($result))
|
||||
return $result;
|
||||
|
||||
$data = getData($result);
|
||||
if (empty($data))
|
||||
return error('Version not found');
|
||||
|
||||
return success($data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
$includesArray = array(
|
||||
'title' => 'CMS',
|
||||
'axios027' => true,
|
||||
'bootstrap5' => true,
|
||||
'fontawesome6' => true,
|
||||
'vue3' => true,
|
||||
'primevue3' => true,
|
||||
'tabulator6' => true,
|
||||
'tinymce5' => true,
|
||||
'phrases' => array('global', 'ui', 'cms'),
|
||||
'customCSSs' => [
|
||||
'public/css/components/verticalsplit.css',
|
||||
'public/css/Cms.css'
|
||||
],
|
||||
'customJSs' => [
|
||||
'vendor/npm-asset/primevue/tree/tree.min.js',
|
||||
'vendor/npm-asset/primevue/checkbox/checkbox.min.js',
|
||||
'vendor/npm-asset/primevue/textarea/textarea.min.js',
|
||||
'vendor/moment/luxonjs/luxon.min.js'
|
||||
],
|
||||
'customJSModules' => ['public/js/apps/Cms.js']
|
||||
);
|
||||
$this->load->view('templates/FHC-Header', $includesArray);
|
||||
?>
|
||||
<div id="main">
|
||||
<router-view
|
||||
cms-root="<?= site_url('cms'); ?>"
|
||||
auth-uid="<?= getAuthUID(); ?>"
|
||||
default-language="<?= defined('DEFAULT_LANGUAGE') ? DEFAULT_LANGUAGE : 'German'; ?>"
|
||||
></router-view>
|
||||
</div>
|
||||
<?php $this->load->view('templates/FHC-Footer', $includesArray); ?>
|
||||
@@ -83,12 +83,86 @@ if (! $rechte->isberechtigt('basis/dms', null, 's', null))
|
||||
return confirm('Möchten Sie das File wirklich löschen?');
|
||||
}
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------------------------------
|
||||
* CIS4 BRIDGE -- ticket 78489, CMS admin on CI3 and Vue
|
||||
* ---------------------------------------------------------------------------------
|
||||
*
|
||||
* WHY THIS CHANGE IS HERE AND NOT SOMEWHERE ELSE
|
||||
*
|
||||
* The new CMS admin (application/controllers/Cms.php, public/js/components/Cms/) uses
|
||||
* TinyMCE 5. This file returns its selection through the TinyMCE 3 API, that is
|
||||
* through the global object tinyMCEPopup:
|
||||
*
|
||||
* tinyMCEPopup.getWindowArg("window") -> the calling window
|
||||
* tinyMCEPopup.getWindowArg("input") -> the target field id in that window
|
||||
* tinyMCEPopup.close() -> close this window
|
||||
*
|
||||
* tinyMCEPopup does not exist in TinyMCE 5. It came with the plugin "inlinepopups",
|
||||
* which TinyMCE 4 removed. TinyMCE 5 uses file_picker_callback instead and expects
|
||||
* the caller to hand back the URL.
|
||||
*
|
||||
* The return code must run IN THIS window, because only this window knows which
|
||||
* dms_id the editor clicked. A wrapper in the new admin cannot catch that: it sees
|
||||
* the opened window, not the click inside it. A proxy controller does not help for
|
||||
* the same reason. The selection happens here.
|
||||
*
|
||||
* WHAT WAS DELIBERATELY NOT CHANGED
|
||||
*
|
||||
* The ticket excludes the DMS input interface, because branches for it are open.
|
||||
* This change respects that:
|
||||
*
|
||||
* - It only adds. The legacy branch below stays line for line and still runs
|
||||
* unchanged as soon as tinyMCEPopup is present.
|
||||
* - It touches exactly one JavaScript function. No PHP, no markup, no query,
|
||||
* no view, no layout.
|
||||
* - It changes none of the four callers of mySubmit.
|
||||
*
|
||||
* When these branches are merged, only this one function needs attention.
|
||||
*
|
||||
* WHY NO GET PARAMETER TELLS THE TWO PATHS APART
|
||||
*
|
||||
* A switch such as ?cis4=1 on open looks obvious, but it does not hold: the file
|
||||
* browser navigates internally (search, paging, category change) and rebuilds its
|
||||
* links from $_REQUEST. An extra parameter is lost on the way, and the bridge would
|
||||
* be dead after the first search. The runtime check through tinyMCEPopup and
|
||||
* window.opener survives every internal navigation and needs no change to the links.
|
||||
*
|
||||
* THE CONTRACT WITH THE NEW ADMIN
|
||||
*
|
||||
* message: { typ: 'fhc-dms-selection', url: 'dms.php?id=<dms_id>' }
|
||||
* target: window.opener
|
||||
* origin: window.location.origin (the receiver checks against it)
|
||||
*
|
||||
* The URL stays RELATIVE. This is mandatory, not carelessness:
|
||||
* - The legacy view runs under /cms/ and resolves dms.php?id=N directly.
|
||||
* - The CIS4 view replaces the string 'dms.php' in CmsLib::getContent() with
|
||||
* APP_ROOT . 'cms/dms.php'.
|
||||
* An absolute URL breaks both paths. Stored content must stay interchangeable
|
||||
* between the two interfaces.
|
||||
*
|
||||
* Receiving side: public/js/components/Cms/Form/FieldWysiwyg.js
|
||||
* ---------------------------------------------------------------------------------
|
||||
*/
|
||||
var FileBrowserDialog=
|
||||
{
|
||||
init: function(){
|
||||
},
|
||||
mySubmit : function (id) {
|
||||
var URL = "dms.php?id="+id;
|
||||
|
||||
// CIS4 path: see the comment block above this function.
|
||||
if (typeof tinyMCEPopup === 'undefined' && window.opener && !window.opener.closed) {
|
||||
window.opener.postMessage({ typ: 'fhc-dms-selection', url: URL }, window.location.origin);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof tinyMCEPopup === 'undefined') {
|
||||
alert('The calling window is no longer available.');
|
||||
return;
|
||||
}
|
||||
|
||||
var win = tinyMCEPopup.getWindowArg("window");
|
||||
|
||||
// insert information now
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.cms-admin {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cms-admin-tree {
|
||||
width: 300px;
|
||||
min-width: 200px;
|
||||
max-width: 400px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid var(--fhc-border, #dee2e6);
|
||||
}
|
||||
|
||||
.cms-admin-main {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cms-admin-tab-content {
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
export default {
|
||||
|
||||
// --- CmsAdmin ---
|
||||
|
||||
getTree(menu, filter) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdmin/getTree',
|
||||
params: { menu, ...(filter ? { filter } : {}) }
|
||||
};
|
||||
},
|
||||
getContent(content_id) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdmin/getContent',
|
||||
params: { content_id }
|
||||
};
|
||||
},
|
||||
getContentsprache(content_id, sprache, version) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdmin/getContentsprache',
|
||||
params: { content_id, sprache, version }
|
||||
};
|
||||
},
|
||||
getTemplates() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdmin/getTemplates'
|
||||
};
|
||||
},
|
||||
getOrganisationseinheiten() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdmin/getOrganisationseinheiten'
|
||||
};
|
||||
},
|
||||
getSprachen() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdmin/getSprachen'
|
||||
};
|
||||
},
|
||||
getUsage(content_id) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdmin/getUsage',
|
||||
params: { content_id }
|
||||
};
|
||||
},
|
||||
postContent(parent_content_id) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdmin/postContent',
|
||||
params: { parent_content_id }
|
||||
};
|
||||
},
|
||||
postTranslation(content_id, sprache, version, target_sprache) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdmin/postTranslation',
|
||||
params: { content_id, sprache, version, target_sprache }
|
||||
};
|
||||
},
|
||||
postVersion(content_id, sprache) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdmin/postVersion',
|
||||
params: { content_id, sprache }
|
||||
};
|
||||
},
|
||||
putProperties(content_id, sprache, version, template_kurzbz, oe_kurzbz, aktiv, menu_open, beschreibung, titel, sichtbar) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdmin/putProperties',
|
||||
params: { content_id, sprache, version, template_kurzbz, oe_kurzbz, aktiv, menu_open, beschreibung, titel, sichtbar }
|
||||
};
|
||||
},
|
||||
deleteContent(content_id) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdmin/deleteContent',
|
||||
params: { content_id }
|
||||
};
|
||||
},
|
||||
deleteContentsprache(content_id, sprache, version) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdmin/deleteContentsprache',
|
||||
params: { content_id, sprache, version }
|
||||
};
|
||||
},
|
||||
|
||||
// --- CmsAdminInhalt ---
|
||||
|
||||
getFormSchema(template_kurzbz) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdminInhalt/getFormSchema',
|
||||
params: { template_kurzbz }
|
||||
};
|
||||
},
|
||||
getFormData(content_id, sprache, version) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdminInhalt/getFormData',
|
||||
params: { content_id, sprache, version }
|
||||
};
|
||||
},
|
||||
getLock(content_id, sprache, version) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdminInhalt/getLock',
|
||||
params: { content_id, sprache, version }
|
||||
};
|
||||
},
|
||||
getVersions(content_id, sprache) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdminInhalt/getVersions',
|
||||
params: { content_id, sprache }
|
||||
};
|
||||
},
|
||||
putFormData(content_id, sprache, version, values) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminInhalt/putFormData',
|
||||
params: { content_id, sprache, version, values }
|
||||
};
|
||||
},
|
||||
postLock(contentsprache_id) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminInhalt/postLock',
|
||||
params: { contentsprache_id }
|
||||
};
|
||||
},
|
||||
deleteLock(contentsprache_id) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminInhalt/deleteLock',
|
||||
params: { contentsprache_id }
|
||||
};
|
||||
},
|
||||
deleteLockForced(contentsprache_id) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminInhalt/deleteLockForced',
|
||||
params: { contentsprache_id }
|
||||
};
|
||||
},
|
||||
|
||||
// --- CmsAdminStruktur ---
|
||||
|
||||
getGruppen(content_id) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/getGruppen',
|
||||
params: { content_id }
|
||||
};
|
||||
},
|
||||
getAllGruppen() {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/getAllGruppen'
|
||||
};
|
||||
},
|
||||
getChilds(content_id, sprache) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/getChilds',
|
||||
params: { content_id, sprache }
|
||||
};
|
||||
},
|
||||
getPossibleChilds(content_id, sprache) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/getPossibleChilds',
|
||||
params: { content_id, sprache }
|
||||
};
|
||||
},
|
||||
postGruppe(content_id, gruppe_kurzbz) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/postGruppe',
|
||||
params: { content_id, gruppe_kurzbz }
|
||||
};
|
||||
},
|
||||
deleteGruppe(content_id, gruppe_kurzbz) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/deleteGruppe',
|
||||
params: { content_id, gruppe_kurzbz }
|
||||
};
|
||||
},
|
||||
postChild(content_id, child_content_id) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/postChild',
|
||||
params: { content_id, child_content_id }
|
||||
};
|
||||
},
|
||||
deleteChild(contentchild_id) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/deleteChild',
|
||||
params: { contentchild_id }
|
||||
};
|
||||
},
|
||||
putChildSort(contentchild_id, direction) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/CmsAdminStruktur/putChildSort',
|
||||
params: { contentchild_id, direction }
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import CmsAdmin from "../components/Cms/CmsAdmin.js";
|
||||
import PluginsPhrasen from "../plugins/Phrasen.js";
|
||||
|
||||
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '')
|
||||
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router;
|
||||
|
||||
const router = VueRouter.createRouter({
|
||||
history: VueRouter.createWebHistory('/' + ciPath),
|
||||
routes: [
|
||||
{
|
||||
name: 'index',
|
||||
path: '/cms',
|
||||
component: CmsAdmin,
|
||||
children: [
|
||||
{
|
||||
name: 'content',
|
||||
path: 'content/:content_id/:sprache/:version/:tab',
|
||||
component: CmsAdmin
|
||||
}
|
||||
]
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: { name: 'index' } }
|
||||
]
|
||||
});
|
||||
|
||||
FhcApps.router.makeExtendable(router);
|
||||
|
||||
const app = Vue.createApp({
|
||||
name: 'CmsApp'
|
||||
});
|
||||
|
||||
FhcApps.makeExtendable(app);
|
||||
|
||||
app
|
||||
.use(router)
|
||||
.use(primevue.config.default, {
|
||||
zIndex: {
|
||||
overlay: 1100
|
||||
}
|
||||
})
|
||||
.use(PluginsPhrasen)
|
||||
.mount('#main');
|
||||
@@ -0,0 +1,213 @@
|
||||
import ApiCmsAdmin from '../../api/factory/cmsadmin.js';
|
||||
import ContentTree from './Tree/ContentTree.js';
|
||||
import ContentHeader from './Header/ContentHeader.js';
|
||||
import Properties from './Tabs/Properties.js';
|
||||
import ContentTab from './Tabs/Content.js';
|
||||
import Permissions from './Tabs/Permissions.js';
|
||||
import Children from './Tabs/Children.js';
|
||||
import History from './Tabs/History.js';
|
||||
import Delete from './Tabs/Delete.js';
|
||||
|
||||
const TAB_COMPONENTS = {
|
||||
properties: Properties,
|
||||
content: ContentTab,
|
||||
permissions: Permissions,
|
||||
children: Children,
|
||||
history: History,
|
||||
delete: Delete
|
||||
};
|
||||
|
||||
const TABS = [
|
||||
{ key: 'properties', phrase: 'cms/tabEigenschaften' },
|
||||
{ key: 'content', phrase: 'cms/tabInhalt' },
|
||||
{ key: 'permissions', phrase: 'cms/tabRechte' },
|
||||
{ key: 'children', phrase: 'cms/tabChilds' },
|
||||
{ key: 'history', phrase: 'cms/tabHistory' },
|
||||
{ key: 'delete', phrase: 'cms/tabLoeschen' }
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'CmsAdmin',
|
||||
components: {
|
||||
'cms-tree': ContentTree,
|
||||
'cms-content-header': ContentHeader,
|
||||
...TAB_COMPONENTS
|
||||
},
|
||||
props: {
|
||||
cmsRoot: String,
|
||||
authUid: String,
|
||||
defaultLanguage: { type: String, default: 'German' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contentId: null,
|
||||
sprache: null,
|
||||
version: null,
|
||||
tab: 'properties',
|
||||
contentInfo: null,
|
||||
treeKey: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeTabComponent() {
|
||||
return TAB_COMPONENTS[this.tab] || null;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.params': {
|
||||
handler(params) {
|
||||
const id = params.content_id ? Number(params.content_id) : null;
|
||||
const sprache = params.sprache || null;
|
||||
const version = params.version ? Number(params.version) : null;
|
||||
const tab = params.tab || 'properties';
|
||||
|
||||
if (id !== this.contentId || sprache !== this.sprache
|
||||
|| version !== this.version || tab !== this.tab) {
|
||||
this.contentId = id;
|
||||
this.sprache = sprache;
|
||||
this.version = version;
|
||||
this.tab = tab;
|
||||
|
||||
if (id !== null && this.contentInfo?.content_id !== id) {
|
||||
this.loadContentInfo();
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadContentInfo() {
|
||||
if (this.contentId === null) {
|
||||
this.contentInfo = null;
|
||||
return;
|
||||
}
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getContent(this.contentId))
|
||||
.then(result => {
|
||||
this.contentInfo = result.data;
|
||||
});
|
||||
},
|
||||
|
||||
selectContent(content_id) {
|
||||
if (content_id === null) {
|
||||
this.contentId = null;
|
||||
this.sprache = null;
|
||||
this.version = null;
|
||||
this.tab = 'properties';
|
||||
this.contentInfo = null;
|
||||
this.$router.push({ name: 'index' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.contentId = content_id;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getContent(content_id))
|
||||
.then(result => {
|
||||
this.contentInfo = result.data;
|
||||
this.preselectLanguageVersion();
|
||||
this.pushRoute();
|
||||
});
|
||||
},
|
||||
|
||||
selectTab(tab) {
|
||||
this.tab = tab;
|
||||
this.pushRoute();
|
||||
},
|
||||
|
||||
selectLanguageVersion(sprache, version) {
|
||||
this.sprache = sprache;
|
||||
this.version = version;
|
||||
this.pushRoute();
|
||||
},
|
||||
|
||||
reloadTree() {
|
||||
this.treeKey++;
|
||||
},
|
||||
|
||||
preselectLanguageVersion() {
|
||||
if (!this.contentInfo) return;
|
||||
|
||||
const languages = this.contentInfo.languages || [];
|
||||
const versions = this.contentInfo.versions || {};
|
||||
|
||||
let lang = languages.includes(this.defaultLanguage)
|
||||
? this.defaultLanguage
|
||||
: languages[0] || null;
|
||||
|
||||
let ver = null;
|
||||
if (lang && versions[lang] && versions[lang].length) {
|
||||
ver = Math.max(...versions[lang]);
|
||||
}
|
||||
|
||||
this.sprache = lang;
|
||||
this.version = ver;
|
||||
},
|
||||
|
||||
pushRoute() {
|
||||
if (this.contentId === null) {
|
||||
this.$router.push({ name: 'index' });
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
name: 'content',
|
||||
params: {
|
||||
content_id: this.contentId,
|
||||
sprache: this.sprache,
|
||||
version: this.version,
|
||||
tab: this.tab
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="cms-admin d-flex h-100">
|
||||
<div class="cms-admin-tree">
|
||||
<cms-tree
|
||||
:key="treeKey"
|
||||
:active-content-id="contentId"
|
||||
@select-content="selectContent"
|
||||
@entry-created="reloadTree"
|
||||
></cms-tree>
|
||||
</div>
|
||||
<div class="cms-admin-main flex-grow-1 d-flex flex-column" v-if="contentId !== null">
|
||||
<cms-content-header
|
||||
:content-id="contentId"
|
||||
:sprache="sprache"
|
||||
:version="version"
|
||||
:contentInfo="contentInfo"
|
||||
@select-language-version="selectLanguageVersion"
|
||||
@reload-content-info="loadContentInfo"
|
||||
@reload-tree="reloadTree"
|
||||
></cms-content-header>
|
||||
<ul class="nav nav-tabs px-3">
|
||||
<li class="nav-item" v-for="t in tabs" :key="t.key">
|
||||
<a class="nav-link"
|
||||
:class="{ active: tab === t.key }"
|
||||
href="#"
|
||||
@click.prevent="selectTab(t.key)"
|
||||
>{{ $p.t(t.phrase) }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="cms-admin-tab-content flex-grow-1 overflow-auto">
|
||||
<component
|
||||
:is="activeTabComponent"
|
||||
:content-id="contentId"
|
||||
:sprache="sprache"
|
||||
:version="version"
|
||||
:contentInfo="contentInfo"
|
||||
@reload-content-info="loadContentInfo"
|
||||
@reload-tree="reloadTree"
|
||||
@select-content="selectContent"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cms-admin-main flex-grow-1 d-flex align-items-center justify-content-center text-muted" v-else>
|
||||
{{ $p.t('cms/keinEintragGewaehlt') }}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
created() {
|
||||
this.tabs = TABS;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
name: 'FieldString',
|
||||
props: {
|
||||
modelValue: { type: String, default: '' },
|
||||
label: String,
|
||||
required: Boolean,
|
||||
disabled: Boolean,
|
||||
type: { type: String, default: 'text' }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
{{ label }}<span v-if="required" class="text-danger"> *</span>
|
||||
</label>
|
||||
<input
|
||||
class="form-control"
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
export default {
|
||||
name: 'FieldWysiwyg',
|
||||
props: {
|
||||
modelValue: { type: String, default: '' },
|
||||
label: String,
|
||||
required: Boolean,
|
||||
disabled: Boolean
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
editorId: 'cms-wysiwyg-' + Math.random().toString(36).substring(2, 11)
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getContent() {
|
||||
return this.editor ? this.editor.getContent() : this.modelValue;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue(newVal) {
|
||||
if (this.editor && this.editor.getContent() !== newVal) {
|
||||
this.editor.setContent(newVal || '');
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const vm = this;
|
||||
tinymce.init({
|
||||
target: this.$refs.editorArea,
|
||||
height: 500,
|
||||
language: 'de',
|
||||
// valid_elements: '*[*]' permits every tag and attribute. Existing content
|
||||
// contains audio, video and source tags. A stricter setting deletes them silently.
|
||||
valid_elements: '*[*]',
|
||||
forced_root_block: '',
|
||||
style_formats: [
|
||||
{ title: 'Contenttabelle', selector: 'table', classes: 'cmstable' },
|
||||
{ title: 'Hauptcontent', block: 'div', classes: 'cmscontent' },
|
||||
{ title: 'Menuebox', block: 'div', classes: 'menubox' },
|
||||
{ title: 'Teambox', block: 'div', classes: 'teambox' },
|
||||
{ title: 'Schatteneffekt Grafiken', selector: 'img', classes: 'screenshot_boxshadow' },
|
||||
{ title: 'Tablesorter', selector: 'table',
|
||||
classes: 'tablesorter tablesorter_col_0 tablesorter_sort_0' }
|
||||
],
|
||||
plugins: 'code lists link image table fullscreen paste searchreplace charmap directionality visualblocks anchor hr',
|
||||
toolbar: [
|
||||
'code | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | styleselect fontsizeselect',
|
||||
'pastetext | searchreplace | bullist numlist | outdent indent blockquote | undo redo | link unlink anchor image | forecolor backcolor',
|
||||
'table | hr removeformat visualblocks | subscript superscript | charmap | ltr rtl | fullscreen'
|
||||
],
|
||||
file_picker_callback: (callback, value, meta) => {
|
||||
if (vm._dmsListener) {
|
||||
window.removeEventListener('message', vm._dmsListener);
|
||||
}
|
||||
|
||||
const url = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cms/tinymce_dms.php';
|
||||
window.open(url, 'DMS', 'width=1400,height=850,scrollbars=1,resizable=1');
|
||||
|
||||
const onMessage = (event) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (!event.data || event.data.typ !== 'fhc-dms-selection') return;
|
||||
// The relative form dms.php?id=N is mandatory. The legacy view runs under /cms/
|
||||
// and resolves it directly. The CIS4 view replaces it in CmsLib::getContent
|
||||
// with APP_ROOT . 'cms/dms.php'. An absolute URL breaks both paths.
|
||||
callback(event.data.url);
|
||||
window.removeEventListener('message', onMessage);
|
||||
vm._dmsListener = null;
|
||||
};
|
||||
vm._dmsListener = onMessage;
|
||||
window.addEventListener('message', onMessage);
|
||||
},
|
||||
setup: (editor) => {
|
||||
vm.editor = editor;
|
||||
editor.on('init', () => {
|
||||
editor.setContent(vm.modelValue || '');
|
||||
if (vm.disabled) editor.mode.set('readonly');
|
||||
});
|
||||
editor.on('change input', () => {
|
||||
vm.$emit('update:modelValue', editor.getContent());
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this._dmsListener) {
|
||||
window.removeEventListener('message', this._dmsListener);
|
||||
this._dmsListener = null;
|
||||
}
|
||||
if (this.editor && tinymce.get(this.editor.id)) {
|
||||
tinymce.get(this.editor.id).remove();
|
||||
this.editor = null;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
{{ label }}<span v-if="required" class="text-danger"> *</span>
|
||||
</label>
|
||||
<textarea ref="editorArea" :id="editorId"></textarea>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import FieldString from './FieldString.js';
|
||||
import FieldWysiwyg from './FieldWysiwyg.js';
|
||||
|
||||
export default {
|
||||
name: 'XsdForm',
|
||||
components: {
|
||||
'field-string': FieldString,
|
||||
'field-wysiwyg': FieldWysiwyg
|
||||
},
|
||||
props: {
|
||||
schema: Object,
|
||||
modelValue: Object,
|
||||
disabled: Boolean
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
updateField(name, value) {
|
||||
this.$emit('update:modelValue', { ...this.modelValue, [name]: value });
|
||||
},
|
||||
collectValues() {
|
||||
const vals = { ...this.modelValue };
|
||||
if (!this.schema?.fields) return vals;
|
||||
for (const field of this.schema.fields) {
|
||||
if (field.type === 'wysiwyg') {
|
||||
const ref = this.$refs['wysiwyg-' + field.name];
|
||||
const comp = Array.isArray(ref) ? ref[0] : ref;
|
||||
if (comp?.getContent) vals[field.name] = comp.getContent();
|
||||
}
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div v-if="schema && schema.fields">
|
||||
<template v-for="field in schema.fields" :key="field.name">
|
||||
<field-wysiwyg
|
||||
v-if="field.type === 'wysiwyg'"
|
||||
:ref="'wysiwyg-' + field.name"
|
||||
:modelValue="modelValue[field.name] || ''"
|
||||
:label="field.name"
|
||||
:required="field.required"
|
||||
:disabled="disabled"
|
||||
@update:modelValue="updateField(field.name, $event)"
|
||||
></field-wysiwyg>
|
||||
<div v-else-if="field.type === 'boolean'" class="mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
:checked="!!modelValue[field.name]"
|
||||
:disabled="disabled"
|
||||
@change="updateField(field.name, $event.target.checked)"
|
||||
:id="'field-' + field.name"
|
||||
>
|
||||
<label class="form-check-label" :for="'field-' + field.name">
|
||||
{{ field.name }}<span v-if="field.required" class="text-danger"> *</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<field-string
|
||||
v-else
|
||||
:modelValue="modelValue[field.name] || ''"
|
||||
:label="field.name"
|
||||
:required="field.required"
|
||||
:disabled="disabled"
|
||||
:type="field.type === 'date' ? 'date' : 'text'"
|
||||
@update:modelValue="updateField(field.name, $event)"
|
||||
></field-string>
|
||||
</template>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
|
||||
import VersionSelect from './VersionSelect.js';
|
||||
import LanguageSelect from './LanguageSelect.js';
|
||||
|
||||
export default {
|
||||
name: 'CmsContentHeader',
|
||||
components: {
|
||||
'version-select': VersionSelect,
|
||||
'sprache-select': LanguageSelect
|
||||
},
|
||||
props: {
|
||||
contentId: Number,
|
||||
sprache: String,
|
||||
version: Number,
|
||||
contentInfo: Object
|
||||
},
|
||||
emits: ['select-language-version', 'reload-content-info', 'reload-tree'],
|
||||
data() {
|
||||
return {
|
||||
titel: '',
|
||||
allSprachen: [],
|
||||
versionDetails: [],
|
||||
newSprache: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
languages() {
|
||||
return this.contentInfo?.languages || [];
|
||||
},
|
||||
versions() {
|
||||
if (!this.contentInfo?.versions || !this.sprache) return [];
|
||||
return this.contentInfo.versions[this.sprache] || [];
|
||||
},
|
||||
availableSprachen() {
|
||||
return this.allSprachen.filter(
|
||||
s => !this.languages.includes(s.sprache)
|
||||
);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
contentId() {
|
||||
this.loadTitel();
|
||||
this.loadVersionDetails();
|
||||
},
|
||||
sprache() {
|
||||
this.loadTitel();
|
||||
this.loadVersionDetails();
|
||||
},
|
||||
version() {
|
||||
this.loadTitel();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadTitel() {
|
||||
if (this.contentId == null || !this.sprache || !this.version) return;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getContentsprache(this.contentId, this.sprache, this.version))
|
||||
.then(result => {
|
||||
this.titel = result.data?.titel || '';
|
||||
});
|
||||
},
|
||||
|
||||
loadVersionDetails() {
|
||||
if (this.contentId == null || !this.sprache) {
|
||||
this.versionDetails = [];
|
||||
return;
|
||||
}
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getVersions(this.contentId, this.sprache))
|
||||
.then(result => {
|
||||
this.versionDetails = result.data || [];
|
||||
});
|
||||
},
|
||||
|
||||
loadSprachen() {
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getSprachen())
|
||||
.then(result => {
|
||||
this.allSprachen = result.data || [];
|
||||
});
|
||||
},
|
||||
|
||||
selectVersion(ver) {
|
||||
this.$emit('select-language-version', this.sprache, ver);
|
||||
},
|
||||
|
||||
selectLanguage(lang) {
|
||||
const versions = this.contentInfo?.versions?.[lang] || [];
|
||||
const maxVersion = versions.length ? Math.max(...versions) : 1;
|
||||
this.$emit('select-language-version', lang, maxVersion);
|
||||
},
|
||||
|
||||
createVersion() {
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.postVersion(this.contentId, this.sprache))
|
||||
.then(result => {
|
||||
const newVersion = result.data.version;
|
||||
this.$fhcAlert.alertInfo(this.$p.t('cms/neueVersionIstUnsichtbar'));
|
||||
this.$emit('reload-content-info');
|
||||
this.$emit('select-language-version', this.sprache, newVersion);
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
createTranslation() {
|
||||
if (!this.newSprache) return;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.postTranslation(
|
||||
this.contentId, this.sprache, this.version, this.newSprache
|
||||
))
|
||||
.then(() => {
|
||||
const lang = this.newSprache;
|
||||
this.newSprache = null;
|
||||
this.$emit('reload-content-info');
|
||||
this.$emit('select-language-version', lang, this.version);
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadSprachen();
|
||||
this.loadTitel();
|
||||
this.loadVersionDetails();
|
||||
},
|
||||
template: `
|
||||
<div class="px-3 pt-2 pb-2 border-bottom" v-if="contentInfo">
|
||||
<div class="mb-2 fw-bold">
|
||||
{{ $p.t('cms/contentId') }}: {{ contentId }}
|
||||
| {{ $p.t('cms/version') }}: {{ version }}
|
||||
| {{ $p.t('cms/sprache') }}: {{ sprache }}
|
||||
| {{ $p.t('cms/titel') }}: {{ titel }}
|
||||
</div>
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<version-select
|
||||
:versions="versions"
|
||||
:version-details="versionDetails"
|
||||
:version="version"
|
||||
@select-version="selectVersion"
|
||||
></version-select>
|
||||
<sprache-select
|
||||
:languages="languages"
|
||||
:sprache="sprache"
|
||||
@select-language="selectLanguage"
|
||||
></sprache-select>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
@click="createVersion"
|
||||
>{{ $p.t('cms/neueVersionAnlegen') }}</button>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1" v-if="availableSprachen.length">
|
||||
<select class="form-select form-select-sm" v-model="newSprache"
|
||||
style="width:auto">
|
||||
<option :value="null" disabled>{{ $p.t('cms/spracheWaehlen') }}</option>
|
||||
<option v-for="s in availableSprachen" :key="s.sprache"
|
||||
:value="s.sprache">{{ s.bezeichnung }}</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
:disabled="!newSprache"
|
||||
@click="createTranslation"
|
||||
>{{ $p.t('cms/uebersetzungAnlegen') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
name: 'SpracheSelect',
|
||||
props: {
|
||||
languages: { type: Array, default: () => [] },
|
||||
sprache: String
|
||||
},
|
||||
emits: ['select-language'],
|
||||
template: `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
v-for="lang in languages"
|
||||
:key="lang"
|
||||
type="button"
|
||||
class="btn"
|
||||
:class="lang === sprache ? 'btn-primary' : 'btn-outline-secondary'"
|
||||
@click="$emit('select-language', lang)"
|
||||
>{{ lang }}</button>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
export default {
|
||||
name: 'VersionSelect',
|
||||
props: {
|
||||
versions: { type: Array, default: () => [] },
|
||||
versionDetails: { type: Array, default: () => [] },
|
||||
version: Number
|
||||
},
|
||||
emits: ['select-version'],
|
||||
computed: {
|
||||
sichtbarMap() {
|
||||
const map = {};
|
||||
for (const v of this.versionDetails) {
|
||||
map[v.version] = v.sichtbar;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isSichtbar(ver) {
|
||||
return this.sichtbarMap[ver] !== false;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
v-for="ver in versions"
|
||||
:key="ver"
|
||||
type="button"
|
||||
class="btn"
|
||||
:class="[
|
||||
ver === version ? 'btn-primary' : 'btn-outline-secondary',
|
||||
{ 'opacity-50': !isSichtbar(ver) }
|
||||
]"
|
||||
@click="$emit('select-version', ver)"
|
||||
>{{ ver }}</button>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
export default {
|
||||
name: 'CmsPreview',
|
||||
props: {
|
||||
contentId: Number,
|
||||
sprache: String,
|
||||
version: Number,
|
||||
templateKurzbz: String,
|
||||
sichtbar: Boolean
|
||||
},
|
||||
computed: {
|
||||
// The preview uses the legacy renderer cms/content.php. It reads xslt_xhtml, which
|
||||
// holds a complete HTML document for all eleven templates. The CIS4 route reads
|
||||
// xslt_xhtml_c4, where four templates hold a whole document instead of a fragment.
|
||||
previewUrl() {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cms/content.php?content_id=' + this.contentId
|
||||
+ '&version=' + this.version
|
||||
+ '&sprache=' + this.sprache
|
||||
+ '&sichtbar=';
|
||||
},
|
||||
// The CIS4 route takes content_id only. Its Vue route is
|
||||
// /CisVue/Cms/Content/:content_id, and extra segments hit the catch-all, which
|
||||
// redirects to the dashboard. CIS4 picks the version itself and the language from
|
||||
// the viewer profile.
|
||||
cis4Url() {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
|
||||
+ '/CisVue/Cms/content/' + this.contentId;
|
||||
},
|
||||
// The redirect template navigates away with JavaScript. In an iframe this can close
|
||||
// the admin page. Therefore this case uses a link and not an iframe.
|
||||
isRedirect() {
|
||||
return this.templateKurzbz === 'redirect';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
neuLaden() {
|
||||
if (this.$refs.previewFrame) {
|
||||
this.$refs.previewFrame.src = this.previewUrl + '&t=' + Date.now();
|
||||
}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="mt-3">
|
||||
<div v-if="sichtbar === false" class="text-danger fw-bold mb-2">
|
||||
{{ $p.t('cms/unsichtbarImLivesystem') }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<a :href="cis4Url" target="_blank">{{ $p.t('cms/inCis4Ansehen') }}</a>
|
||||
</div>
|
||||
<template v-if="isRedirect">
|
||||
<a :href="previewUrl" target="_blank">
|
||||
{{ $p.t('cms/vorschauInEigenemFenster') }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<iframe
|
||||
ref="previewFrame"
|
||||
:src="previewUrl"
|
||||
style="width: 100%; min-height: 500px; border: 1px solid #ccc;"
|
||||
></iframe>
|
||||
</template>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
|
||||
|
||||
export default {
|
||||
name: 'CmsChildren',
|
||||
props: {
|
||||
contentId: Number,
|
||||
sprache: String,
|
||||
version: Number,
|
||||
contentInfo: Object
|
||||
},
|
||||
emits: ['reload-content-info', 'reload-tree', 'select-content'],
|
||||
data() {
|
||||
return {
|
||||
childs: [],
|
||||
possibleChilds: [],
|
||||
selectedChild: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// tbl_contentchild has no unique index. The server permits a duplicate assignment.
|
||||
// Therefore the select list hides the children that are already assigned.
|
||||
availableChilds() {
|
||||
const assigned = new Set(this.childs.map(c => c.child_content_id));
|
||||
return this.possibleChilds.filter(c => !assigned.has(c.content_id));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
contentId() { this.loadAll(); },
|
||||
sprache() { this.loadAll(); }
|
||||
},
|
||||
methods: {
|
||||
loadAll() {
|
||||
this.loadChilds();
|
||||
this.loadPossibleChilds();
|
||||
},
|
||||
|
||||
loadChilds() {
|
||||
if (this.contentId == null || !this.sprache) return;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getChilds(this.contentId, this.sprache))
|
||||
.then(result => { this.childs = result.data || []; })
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
loadPossibleChilds() {
|
||||
if (this.contentId == null || !this.sprache) return;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getPossibleChilds(this.contentId, this.sprache))
|
||||
.then(result => {
|
||||
this.possibleChilds = result.data || [];
|
||||
this.selectedChild = '';
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
childLabel(c) {
|
||||
const titel = c.titel || this.$p.t('cms/ohneTitel');
|
||||
return titel + ' (' + c.content_id + ')';
|
||||
},
|
||||
|
||||
async addChild() {
|
||||
if (!this.selectedChild) return;
|
||||
try {
|
||||
await this.$api.call(
|
||||
ApiCmsAdmin.postChild(this.contentId, this.selectedChild)
|
||||
);
|
||||
this.loadAll();
|
||||
this.$emit('reload-tree');
|
||||
} catch (e) {
|
||||
this.$fhcAlert.handleSystemError(e);
|
||||
}
|
||||
},
|
||||
|
||||
async removeChild(contentchild_id) {
|
||||
const confirmed = await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('cms/zuordnungEntfernenBestaetigen')
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await this.$api.call(ApiCmsAdmin.deleteChild(contentchild_id));
|
||||
this.loadAll();
|
||||
this.$emit('reload-tree');
|
||||
} catch (e) {
|
||||
this.$fhcAlert.handleSystemError(e);
|
||||
}
|
||||
},
|
||||
|
||||
async moveSort(contentchild_id, direction) {
|
||||
try {
|
||||
await this.$api.call(
|
||||
ApiCmsAdmin.putChildSort(contentchild_id, direction)
|
||||
);
|
||||
this.loadChilds();
|
||||
this.$emit('reload-tree');
|
||||
} catch (e) {
|
||||
this.$fhcAlert.handleSystemError(e);
|
||||
}
|
||||
},
|
||||
|
||||
onChildClick(child_content_id) {
|
||||
this.$emit('select-content', child_content_id);
|
||||
},
|
||||
|
||||
isFirst(index) {
|
||||
return index === 0;
|
||||
},
|
||||
|
||||
isLast(index) {
|
||||
return index === this.childs.length - 1;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadAll();
|
||||
},
|
||||
template: `
|
||||
<div class="p-3" v-if="contentId != null">
|
||||
<h5>{{ $p.t('cms/folgendeEintraegeSindUntergeordnet') }}</h5>
|
||||
|
||||
<template v-if="childs.length > 0">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $p.t('cms/sortierung') }}</th>
|
||||
<th>ID</th>
|
||||
<th>{{ $p.t('cms/titel') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(child, idx) in childs" :key="child.contentchild_id">
|
||||
<td class="text-nowrap">
|
||||
{{ child.sort }}
|
||||
<button class="btn btn-sm btn-link p-0 ms-1"
|
||||
:disabled="isFirst(idx)"
|
||||
@click="moveSort(child.contentchild_id, 'up')">
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-link p-0 ms-1"
|
||||
:disabled="isLast(idx)"
|
||||
@click="moveSort(child.contentchild_id, 'down')">
|
||||
<i class="fa-solid fa-arrow-down"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ child.child_content_id }}</td>
|
||||
<td>
|
||||
<a href="#" @click.prevent="onChildClick(child.child_content_id)">
|
||||
{{ child.titel || $p.t('cms/ohneTitel') }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
@click="removeChild(child.contentchild_id)">
|
||||
{{ $p.t('cms/entfernen') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<div v-else class="text-muted">
|
||||
{{ $p.t('cms/keineUntereintraege') }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 align-items-end mt-3">
|
||||
<select class="form-select" v-model="selectedChild" style="max-width: 500px;">
|
||||
<option value="" disabled></option>
|
||||
<option v-for="c in availableChilds" :key="c.content_id"
|
||||
:value="c.content_id">
|
||||
{{ childLabel(c) }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" @click="addChild" :disabled="!selectedChild">
|
||||
{{ $p.t('cms/hinzufuegen') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
|
||||
import XsdForm from '../Form/XsdForm.js';
|
||||
import CmsPreview from '../Preview.js';
|
||||
|
||||
export default {
|
||||
name: 'CmsContent',
|
||||
components: {
|
||||
'xsd-form': XsdForm,
|
||||
'cms-preview': CmsPreview
|
||||
},
|
||||
props: {
|
||||
contentId: Number,
|
||||
sprache: String,
|
||||
version: Number,
|
||||
contentInfo: Object
|
||||
},
|
||||
emits: ['reload-content-info', 'reload-tree', 'select-content'],
|
||||
data() {
|
||||
return {
|
||||
schema: null,
|
||||
values: {},
|
||||
sperre: null,
|
||||
sichtbar: true,
|
||||
loading: false,
|
||||
saving: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lockState() {
|
||||
if (!this.sperre || this.sperre.gesperrt_uid === null) return 'free';
|
||||
if (this.sperre.own) return 'own';
|
||||
return 'foreign';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
contentId() { this.loadFormData(); },
|
||||
sprache() { this.loadFormData(); },
|
||||
version() { this.loadFormData(); }
|
||||
},
|
||||
methods: {
|
||||
loadFormData() {
|
||||
if (this.contentId == null || !this.sprache || !this.version) return;
|
||||
this.loading = true;
|
||||
Promise.all([
|
||||
this.$api.call(ApiCmsAdmin.getFormData(this.contentId, this.sprache, this.version)),
|
||||
this.$api.call(ApiCmsAdmin.getContentsprache(this.contentId, this.sprache, this.version))
|
||||
])
|
||||
.then(([formResult, spracheResult]) => {
|
||||
const d = formResult.data;
|
||||
this.schema = d.schema;
|
||||
this.values = d.values || {};
|
||||
this.sperre = d.sperre;
|
||||
this.sichtbar = !!spracheResult.data.sichtbar;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError)
|
||||
.finally(() => { this.loading = false; });
|
||||
},
|
||||
|
||||
lock() {
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.postLock(this.sperre.contentsprache_id))
|
||||
.then(() => { this.loadFormData(); })
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
// LEGACY-QUIRK: Releasing a lock releases all locks of this user, not just this page's. See Q1.
|
||||
unlock() {
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.deleteLock(this.sperre.contentsprache_id))
|
||||
.then(() => { this.loadFormData(); })
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
forceUnlock() {
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.deleteLockForced(this.sperre.contentsprache_id))
|
||||
.then(() => { this.loadFormData(); })
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
save() {
|
||||
const freshValues = this.$refs.xsdForm
|
||||
? this.$refs.xsdForm.collectValues()
|
||||
: this.values;
|
||||
this.saving = true;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.putFormData(
|
||||
this.contentId, this.sprache, this.version, freshValues
|
||||
))
|
||||
.then(() => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('cms/gespeichert'));
|
||||
if (this.$refs.preview) this.$refs.preview.neuLaden();
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError)
|
||||
.finally(() => { this.saving = false; });
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadFormData();
|
||||
},
|
||||
template: `
|
||||
<div class="p-3" v-if="contentId != null">
|
||||
<template v-if="!loading && sperre">
|
||||
<template v-if="lockState === 'free'">
|
||||
<button class="btn btn-primary mb-3" @click="lock">
|
||||
{{ $p.t('cms/zurBearbeitungSperren') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="lockState === 'own'">
|
||||
<div class="alert alert-warning">
|
||||
{{ $p.t('cms/freigabeGibtAlleSperrenFrei') }}
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary mb-3" @click="unlock">
|
||||
{{ $p.t('cms/sperreFreigeben') }}
|
||||
</button>
|
||||
<xsd-form
|
||||
ref="xsdForm"
|
||||
:schema="schema"
|
||||
v-model="values"
|
||||
:disabled="false"
|
||||
></xsd-form>
|
||||
<button class="btn btn-primary mt-2 mb-3" @click="save" :disabled="saving">
|
||||
{{ $p.t('cms/speichern') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="alert alert-info">
|
||||
{{ $p.t('cms/gesperrtVonSeit', { uid: sperre.gesperrt_uid, start: sperre.start }) }}
|
||||
</div>
|
||||
<button v-if="sperre.may_force" class="btn btn-danger mb-3" @click="forceUnlock">
|
||||
{{ $p.t('cms/freigabeErzwingen') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<cms-preview
|
||||
ref="preview"
|
||||
:content-id="contentId"
|
||||
:sprache="sprache"
|
||||
:version="version"
|
||||
:template-kurzbz="contentInfo?.template_kurzbz"
|
||||
:sichtbar="sichtbar"
|
||||
></cms-preview>
|
||||
</template>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
|
||||
|
||||
export default {
|
||||
name: 'CmsDelete',
|
||||
props: {
|
||||
contentId: Number,
|
||||
sprache: String,
|
||||
version: Number,
|
||||
contentInfo: Object
|
||||
},
|
||||
emits: ['reload-content-info', 'reload-tree', 'select-content'],
|
||||
data() {
|
||||
return {
|
||||
usages: [],
|
||||
loadingUsage: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasRestrict() {
|
||||
return this.usages.some(u => u.table !== 'campus.tbl_infoscreen_content');
|
||||
},
|
||||
// Request to T07: forward defaultLanguage prop to tabs. Using 'German' as fallback.
|
||||
isLastVersion() {
|
||||
if (!this.contentInfo?.versions || !this.sprache) return false;
|
||||
const defaultLang = 'German';
|
||||
const versions = this.contentInfo.versions[this.sprache];
|
||||
return this.sprache === defaultLang && versions && versions.length === 1;
|
||||
},
|
||||
totalVersionCount() {
|
||||
if (!this.contentInfo?.versions) return 0;
|
||||
return Object.values(this.contentInfo.versions)
|
||||
.reduce((sum, vers) => sum + vers.length, 0);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
contentId() { this.loadUsage(); }
|
||||
},
|
||||
methods: {
|
||||
loadUsage() {
|
||||
if (this.contentId == null) return;
|
||||
this.loadingUsage = true;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getUsage(this.contentId))
|
||||
.then(result => {
|
||||
this.usages = result.data || [];
|
||||
})
|
||||
.finally(() => { this.loadingUsage = false; });
|
||||
},
|
||||
|
||||
deleteVersion() {
|
||||
this.$fhcAlert.confirm({
|
||||
message: this.$p.t('cms/versionLoeschenBestaetigen'),
|
||||
acceptLabel: this.$p.t('ui/loeschen'),
|
||||
acceptClass: 'p-button-danger'
|
||||
}).then(confirmed => {
|
||||
if (!confirmed) return;
|
||||
return this.$api.call(
|
||||
ApiCmsAdmin.deleteContentsprache(this.contentId, this.sprache, this.version)
|
||||
);
|
||||
}).then(result => {
|
||||
if (!result) return;
|
||||
this.$emit('reload-content-info');
|
||||
}).catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
deleteContent() {
|
||||
this.$fhcAlert.confirm({
|
||||
message: this.$p.t('cms/contentLoeschenBestaetigen', {
|
||||
sprachen: this.contentInfo.languages.length,
|
||||
versionen: this.totalVersionCount
|
||||
}),
|
||||
acceptLabel: this.$p.t('ui/loeschen'),
|
||||
acceptClass: 'p-button-danger'
|
||||
}).then(confirmed => {
|
||||
if (!confirmed) return;
|
||||
return this.$api.call(ApiCmsAdmin.deleteContent(this.contentId));
|
||||
}).then(result => {
|
||||
if (!result) return;
|
||||
this.$emit('reload-tree');
|
||||
this.$nextTick(() => {
|
||||
this.$emit('select-content', null);
|
||||
});
|
||||
}).catch(this.$fhcAlert.handleSystemError);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadUsage();
|
||||
},
|
||||
// Five tables prevent the deletion with ON DELETE RESTRICT. tbl_infoscreen_content
|
||||
// deletes its own rows with CASCADE. The legacy code shows neither case and hides the
|
||||
// error. See Q5 in the contract.
|
||||
template: `
|
||||
<div class="p-3" v-if="contentId != null">
|
||||
<h5>{{ $p.t('cms/dieseVersionLoeschen') }}</h5>
|
||||
|
||||
<div v-if="isLastVersion" class="alert alert-warning mb-3">
|
||||
{{ $p.t('cms/letzteVersionNichtLoeschbar') }}
|
||||
</div>
|
||||
<div v-else class="mb-3">
|
||||
<button class="btn btn-outline-danger" @click="deleteVersion">
|
||||
{{ $p.t('cms/dieseVersionLoeschen') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5>{{ $p.t('cms/ganzenContentLoeschen') }}</h5>
|
||||
|
||||
<div v-if="loadingUsage" class="mb-3">
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="usages.length === 0" class="alert alert-info mb-3">
|
||||
{{ $p.t('cms/keineVerwendungGefunden') }}
|
||||
</div>
|
||||
<div v-else class="mb-3">
|
||||
<div class="alert alert-warning">
|
||||
{{ $p.t('cms/contentWirdVerwendetIn') }}
|
||||
</div>
|
||||
<ul class="list-group mb-3">
|
||||
<li v-for="(u, i) in usages" :key="i"
|
||||
class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>{{ u.label }}</span>
|
||||
<span v-if="u.table === 'campus.tbl_infoscreen_content'"
|
||||
class="badge bg-danger">
|
||||
{{ $p.t('cms/wirdMitgeloescht') }}
|
||||
</span>
|
||||
<span v-else class="badge bg-warning text-dark">
|
||||
{{ $p.t('cms/verhindertLoeschen') }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button class="btn btn-danger" @click="deleteContent"
|
||||
:disabled="hasRestrict || loadingUsage">
|
||||
{{ $p.t('cms/ganzenContentLoeschen') }}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
|
||||
|
||||
export default {
|
||||
name: 'CmsHistory',
|
||||
props: {
|
||||
contentId: Number,
|
||||
sprache: String,
|
||||
version: Number,
|
||||
contentInfo: Object
|
||||
},
|
||||
emits: ['reload-content-info', 'reload-tree', 'select-content'],
|
||||
data() {
|
||||
return {
|
||||
versions: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
contentId() { this.loadVersions(); },
|
||||
sprache() { this.loadVersions(); }
|
||||
},
|
||||
methods: {
|
||||
loadVersions() {
|
||||
if (this.contentId == null || !this.sprache) return;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getVersions(this.contentId, this.sprache))
|
||||
.then(result => {
|
||||
this.versions = result.data || [];
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const dt = luxon.DateTime.fromSQL(dateStr);
|
||||
return dt.isValid ? dt.toFormat('dd.MM.yyyy') : dateStr;
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadVersions();
|
||||
},
|
||||
template: `
|
||||
<div class="p-3" v-if="contentId != null">
|
||||
<h5>{{ $p.t('cms/versionen') }}</h5>
|
||||
|
||||
<div v-for="v in versions" :key="v.version"
|
||||
class="mb-3 p-2 border rounded"
|
||||
:class="{ 'border-primary border-2': v.version === version }">
|
||||
<div class="fw-bold">
|
||||
{{ $p.t('cms/version') }} {{ v.version }}
|
||||
<span v-if="!v.sichtbar" class="badge bg-secondary ms-2">
|
||||
{{ $p.t('cms/unsichtbar') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>{{ $p.t('cms/erstelltAmVon', { amum: formatDate(v.insertamum), von: v.insertvon }) }}</div>
|
||||
<div v-if="v.updatevon">
|
||||
{{ $p.t('cms/letzteAenderungVonAm', { von: v.updatevon, amum: formatDate(v.updateamum) }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
|
||||
|
||||
export default {
|
||||
name: 'CmsPermissions',
|
||||
props: {
|
||||
contentId: Number,
|
||||
sprache: String,
|
||||
version: Number,
|
||||
contentInfo: Object
|
||||
},
|
||||
emits: ['reload-content-info', 'reload-tree', 'select-content'],
|
||||
data() {
|
||||
return {
|
||||
gruppen: [],
|
||||
allGruppen: [],
|
||||
selectedGruppe: '',
|
||||
allGruppenLoaded: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
availableGruppen() {
|
||||
const assigned = new Set(this.gruppen.map(g => g.gruppe_kurzbz));
|
||||
return this.allGruppen.filter(g => !assigned.has(g.gruppe_kurzbz));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
contentId() { this.loadGruppen(); }
|
||||
},
|
||||
methods: {
|
||||
loadGruppen() {
|
||||
if (this.contentId == null) return;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getGruppen(this.contentId))
|
||||
.then(result => {
|
||||
this.gruppen = result.data || [];
|
||||
this.selectedGruppe = '';
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
loadAllGruppen() {
|
||||
if (this.allGruppenLoaded) return;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getAllGruppen())
|
||||
.then(result => {
|
||||
this.allGruppen = result.data || [];
|
||||
this.allGruppenLoaded = true;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
|
||||
async addGruppe() {
|
||||
if (!this.selectedGruppe) return;
|
||||
try {
|
||||
await this.$api.call(
|
||||
ApiCmsAdmin.postGruppe(this.contentId, this.selectedGruppe)
|
||||
);
|
||||
this.loadGruppen();
|
||||
this.$emit('reload-tree');
|
||||
} catch (e) {
|
||||
this.$fhcAlert.handleSystemError(e);
|
||||
}
|
||||
},
|
||||
|
||||
async removeGruppe(gruppe_kurzbz) {
|
||||
if (this.gruppen.length <= 1) {
|
||||
const confirmed = await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('cms/letzteGruppeEntfernenBestaetigen')
|
||||
});
|
||||
if (!confirmed) return;
|
||||
}
|
||||
try {
|
||||
await this.$api.call(
|
||||
ApiCmsAdmin.deleteGruppe(this.contentId, gruppe_kurzbz)
|
||||
);
|
||||
this.loadGruppen();
|
||||
this.$emit('reload-tree');
|
||||
} catch (e) {
|
||||
this.$fhcAlert.handleSystemError(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadAllGruppen();
|
||||
this.loadGruppen();
|
||||
},
|
||||
template: `
|
||||
<div class="p-3" v-if="contentId != null">
|
||||
<template v-if="gruppen.length > 0">
|
||||
<h5>{{ $p.t('cms/folgendeGruppenDuerfenAnsehen') }}</h5>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $p.t('cms/gruppe') }}</th>
|
||||
<th>{{ $p.t('cms/bezeichnung') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="g in gruppen" :key="g.gruppe_kurzbz">
|
||||
<td>{{ g.gruppe_kurzbz }}</td>
|
||||
<td>{{ g.bezeichnung }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
@click="removeGruppe(g.gruppe_kurzbz)">
|
||||
{{ $p.t('cms/entfernen') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<div v-else class="alert alert-warning">
|
||||
{{ $p.t('cms/seiteDarfVonAllenAngezeigtWerden') }}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 align-items-end mt-3">
|
||||
<select class="form-select" v-model="selectedGruppe" style="max-width: 400px;">
|
||||
<option value="" disabled></option>
|
||||
<option v-for="g in availableGruppen" :key="g.gruppe_kurzbz"
|
||||
:value="g.gruppe_kurzbz">
|
||||
{{ g.gruppe_kurzbz }} — {{ g.bezeichnung }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" @click="addGruppe" :disabled="!selectedGruppe">
|
||||
{{ $p.t('cms/hinzufuegen') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
|
||||
|
||||
export default {
|
||||
name: 'CmsProperties',
|
||||
props: {
|
||||
contentId: Number,
|
||||
sprache: String,
|
||||
version: Number,
|
||||
contentInfo: Object
|
||||
},
|
||||
emits: ['reload-content-info', 'reload-tree', 'select-content'],
|
||||
data() {
|
||||
return {
|
||||
titel: '',
|
||||
sichtbar: false,
|
||||
template_kurzbz: '',
|
||||
oe_kurzbz: '',
|
||||
aktiv: false,
|
||||
menu_open: false,
|
||||
beschreibung: '',
|
||||
templates: [],
|
||||
organisationseinheiten: [],
|
||||
templateChanged: false,
|
||||
saving: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
contentId() { this.loadData(); },
|
||||
sprache() { this.loadData(); },
|
||||
version() { this.loadData(); },
|
||||
contentInfo() { this.loadContentFields(); }
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.templateChanged = false;
|
||||
if (this.contentId == null || !this.sprache || !this.version) return;
|
||||
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getContentsprache(this.contentId, this.sprache, this.version))
|
||||
.then(result => {
|
||||
const d = result.data;
|
||||
this.titel = d.titel || '';
|
||||
this.sichtbar = !!d.sichtbar;
|
||||
});
|
||||
|
||||
this.loadContentFields();
|
||||
},
|
||||
|
||||
loadContentFields() {
|
||||
if (!this.contentInfo) return;
|
||||
this.template_kurzbz = this.contentInfo.template_kurzbz || '';
|
||||
this.oe_kurzbz = this.contentInfo.oe_kurzbz || '';
|
||||
this.aktiv = !!this.contentInfo.aktiv;
|
||||
this.menu_open = !!this.contentInfo.menu_open;
|
||||
this.beschreibung = this.contentInfo.beschreibung || '';
|
||||
},
|
||||
|
||||
loadLookups() {
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getTemplates())
|
||||
.then(result => { this.templates = result.data || []; });
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getOrganisationseinheiten())
|
||||
.then(result => { this.organisationseinheiten = result.data || []; });
|
||||
},
|
||||
|
||||
onTemplateChange() {
|
||||
this.templateChanged = true;
|
||||
},
|
||||
|
||||
oeLabel(oe) {
|
||||
let label = oe.organisationseinheittyp_kurzbz + ' ' + oe.bezeichnung;
|
||||
if (!oe.aktiv) label += ' (' + this.$p.t('cms/inaktiv') + ')';
|
||||
return label;
|
||||
},
|
||||
|
||||
save() {
|
||||
this.saving = true;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.putProperties(
|
||||
this.contentId, this.sprache, this.version,
|
||||
this.template_kurzbz, this.oe_kurzbz,
|
||||
this.aktiv, this.menu_open, this.beschreibung,
|
||||
this.titel, this.sichtbar
|
||||
))
|
||||
.then(() => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('cms/gespeichert'));
|
||||
this.$emit('reload-content-info');
|
||||
this.$emit('reload-tree');
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError)
|
||||
.finally(() => { this.saving = false; });
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadLookups();
|
||||
this.loadData();
|
||||
},
|
||||
template: `
|
||||
<div class="p-3" v-if="contentId != null">
|
||||
<h5>{{ $p.t('cms/giltFuerAlleSprachen') }}</h5>
|
||||
<div class="mb-3 row">
|
||||
<label class="col-sm-3 col-form-label">{{ $p.t('cms/vorlage') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-select" v-model="template_kurzbz"
|
||||
@change="onTemplateChange">
|
||||
<option v-for="t in templates" :key="t.template_kurzbz"
|
||||
:value="t.template_kurzbz">{{ t.bezeichnung }}</option>
|
||||
</select>
|
||||
<div v-if="templateChanged" class="text-danger mt-1 fw-bold">
|
||||
{{ $p.t('cms/vorlageWarnung') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 row">
|
||||
<label class="col-sm-3 col-form-label">{{ $p.t('cms/organisationseinheit') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-select" v-model="oe_kurzbz">
|
||||
<option v-for="oe in organisationseinheiten" :key="oe.oe_kurzbz"
|
||||
:value="oe.oe_kurzbz"
|
||||
:class="{ 'text-muted': !oe.aktiv }">{{ oeLabel(oe) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="aktiv"
|
||||
id="prop-aktiv">
|
||||
<label class="form-check-label" for="prop-aktiv">
|
||||
{{ $p.t('cms/aktiv') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="menu_open"
|
||||
id="prop-menu-open">
|
||||
<label class="form-check-label" for="prop-menu-open">
|
||||
{{ $p.t('cms/menuOpen') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 row">
|
||||
<label class="col-sm-3 col-form-label">{{ $p.t('cms/beschreibung') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<textarea class="form-control" rows="3" v-model="beschreibung"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h5>{{ $p.t('cms/giltFuerDieseVersion') }}</h5>
|
||||
<div class="mb-3 row">
|
||||
<label class="col-sm-3 col-form-label">{{ $p.t('cms/titel') }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" v-model="titel" maxlength="256">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="sichtbar"
|
||||
id="prop-sichtbar">
|
||||
<label class="form-check-label" for="prop-sichtbar">
|
||||
{{ $p.t('cms/sichtbar') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button class="btn btn-primary" @click="save" :disabled="saving">
|
||||
{{ $p.t('cms/speichern') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
import ApiCmsAdmin from '../../../api/factory/cmsadmin.js';
|
||||
import TreeFilter from './TreeFilter.js';
|
||||
import MenuSwitch from './MenuSwitch.js';
|
||||
|
||||
const TreeNode = {
|
||||
name: 'CmsTreeNode',
|
||||
inject: ['treeSelect', 'treeToggle'],
|
||||
props: {
|
||||
node: Object,
|
||||
activeContentId: Number,
|
||||
expandedKeys: Object,
|
||||
flat: Boolean
|
||||
},
|
||||
computed: {
|
||||
hasChildren() {
|
||||
return this.node.children && this.node.children.length > 0;
|
||||
},
|
||||
isExpanded() {
|
||||
return !!this.expandedKeys[this.node.content_id];
|
||||
},
|
||||
isActive() {
|
||||
return this.activeContentId != null
|
||||
&& this.node.content_id === this.activeContentId;
|
||||
},
|
||||
labelStyle() {
|
||||
const s = {};
|
||||
if (this.isActive) {
|
||||
s.fontWeight = 'bold';
|
||||
s.background = '#e8f0fe';
|
||||
s.borderRadius = '3px';
|
||||
s.padding = '0 4px';
|
||||
}
|
||||
if (!this.node.aktiv) {
|
||||
s.opacity = '0.5';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<li class="py-1">
|
||||
<span v-if="!flat && hasChildren"
|
||||
style="cursor:pointer;user-select:none"
|
||||
@click="treeToggle(node)">
|
||||
<i class="fa-solid fa-fw"
|
||||
:class="isExpanded ? 'fa-caret-down' : 'fa-caret-right'"></i>
|
||||
</span>
|
||||
<span v-else-if="!flat" style="display:inline-block;width:1.25em"></span>
|
||||
<i v-if="node.groups && node.groups.length"
|
||||
class="fa-solid fa-lock fa-sm me-1"
|
||||
:title="node.groups.join(', ')"></i>
|
||||
<a v-if="node.entitled"
|
||||
href="#"
|
||||
class="text-decoration-none"
|
||||
:style="labelStyle"
|
||||
@click.prevent="treeSelect(node)">{{ node.titel }} ({{ node.content_id }})</a>
|
||||
<span v-else :style="!node.aktiv ? 'opacity:0.5' : ''">{{ node.titel }} ({{ node.content_id }})</span>
|
||||
<ul v-if="!flat && isExpanded && hasChildren" class="list-unstyled ms-3">
|
||||
<cms-tree-node
|
||||
v-for="child in node.children"
|
||||
:key="child.content_id"
|
||||
:node="child"
|
||||
:active-content-id="activeContentId"
|
||||
:expanded-keys="expandedKeys"
|
||||
:flat="flat"
|
||||
></cms-tree-node>
|
||||
</ul>
|
||||
</li>
|
||||
`
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'CmsTree',
|
||||
components: {
|
||||
'cms-tree-node': TreeNode,
|
||||
'tree-filter': TreeFilter,
|
||||
'menu-switch': MenuSwitch
|
||||
},
|
||||
props: {
|
||||
activeContentId: Number
|
||||
},
|
||||
emits: ['select-content', 'entry-created'],
|
||||
data() {
|
||||
const stored = localStorage.getItem('cms/menu');
|
||||
return {
|
||||
menu: (stored === 'content' || stored === 'news') ? stored : 'content',
|
||||
filter: '',
|
||||
nodes: [],
|
||||
loading: false,
|
||||
expandedKeys: {}
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
treeSelect: (node) => { this.selectNode(node); },
|
||||
treeToggle: (node) => { this.toggleNode(node); }
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
menu() {
|
||||
this.loadTree();
|
||||
},
|
||||
activeContentId(id) {
|
||||
if (id != null) {
|
||||
this.expandPathTo(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadTree() {
|
||||
this.loading = true;
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.getTree(this.menu, this.filter))
|
||||
.then(result => {
|
||||
|
||||
this.nodes = result.data;
|
||||
this.expandedKeys = {};
|
||||
if (this.activeContentId != null) {
|
||||
this.expandPathTo(this.activeContentId);
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
expandPathTo(targetId) {
|
||||
const search = (nodes) => {
|
||||
for (const node of nodes) {
|
||||
if (node.content_id === targetId) return true;
|
||||
if (node.children && node.children.length) {
|
||||
if (search(node.children)) {
|
||||
this.expandedKeys[node.content_id] = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
search(this.nodes);
|
||||
},
|
||||
toggleNode(node) {
|
||||
if (this.expandedKeys[node.content_id]) {
|
||||
delete this.expandedKeys[node.content_id];
|
||||
} else {
|
||||
this.expandedKeys[node.content_id] = true;
|
||||
}
|
||||
},
|
||||
selectNode(node) {
|
||||
if (node.entitled) {
|
||||
this.$emit('select-content', node.content_id);
|
||||
}
|
||||
},
|
||||
onFilter(text) {
|
||||
this.filter = text;
|
||||
this.loadTree();
|
||||
},
|
||||
createEntry(parentId) {
|
||||
this.$api
|
||||
.call(ApiCmsAdmin.postContent(parentId))
|
||||
.then(result => {
|
||||
const newId = result.data.content_id;
|
||||
this.$emit('entry-created');
|
||||
this.$emit('select-content', newId);
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadTree();
|
||||
},
|
||||
template: `
|
||||
<div class="p-2">
|
||||
<menu-switch v-model="menu"></menu-switch>
|
||||
<tree-filter class="mt-2" @filter="onFilter"></tree-filter>
|
||||
<div class="mt-2 mb-2">
|
||||
<button class="btn btn-sm btn-outline-secondary me-1"
|
||||
@click="createEntry(null)">
|
||||
{{ $p.t('cms/neuenEintragHinzufuegen') }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
:disabled="activeContentId == null"
|
||||
@click="createEntry(activeContentId)">
|
||||
{{ $p.t('cms/neuenChildEintragHinzufuegen') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="loading" class="text-center text-muted py-3">
|
||||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
<div v-else-if="nodes?.length === 0" class="text-muted py-2">
|
||||
{{ filter ? $p.t('cms/keineTreffer') : $p.t('cms/keineEintraege') }}
|
||||
</div>
|
||||
<ul v-else class="list-unstyled mb-0">
|
||||
<cms-tree-node
|
||||
v-for="node in nodes"
|
||||
:key="node.content_id"
|
||||
:node="node"
|
||||
:active-content-id="activeContentId"
|
||||
:expanded-keys="expandedKeys"
|
||||
:flat="menu === 'news'"
|
||||
></cms-tree-node>
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
name: 'CmsMenuSwitch',
|
||||
props: {
|
||||
modelValue: { type: String, default: 'content' }
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
select(value) {
|
||||
localStorage.setItem('cms/menu', value);
|
||||
this.$emit('update:modelValue', value);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// The legacy code keeps this choice in $_SESSION['cms/menu']. The Vue client uses
|
||||
// localStorage. The effect for the editor is the same.
|
||||
const stored = localStorage.getItem('cms/menu');
|
||||
if ((stored === 'content' || stored === 'news') && stored !== this.modelValue) {
|
||||
this.$emit('update:modelValue', stored);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="btn-group btn-group-sm w-100" role="group">
|
||||
<button type="button" class="btn"
|
||||
:class="modelValue === 'content' ? 'btn-primary' : 'btn-outline-secondary'"
|
||||
@click="select('content')">
|
||||
{{ $p.t('cms/menuContent') }}
|
||||
</button>
|
||||
<button type="button" class="btn"
|
||||
:class="modelValue === 'news' ? 'btn-primary' : 'btn-outline-secondary'"
|
||||
@click="select('news')">
|
||||
{{ $p.t('cms/menuNews') }}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
name: 'CmsTreeFilter',
|
||||
emits: ['filter'],
|
||||
data() {
|
||||
return {
|
||||
text: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.$emit('filter', this.text);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
:placeholder="$p.t('cms/filterPlatzhalter')"
|
||||
v-model="text"
|
||||
@keydown.enter="submit">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="submit">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user