Merge branch 'feature-68957/CIS4_Dashboard_Bookmark_Widget_Sort_Tags' into cis40_2026-02_rc

This commit is contained in:
Harald Bamberger
2026-04-23 11:19:17 +02:00
10 changed files with 940 additions and 34 deletions
@@ -29,9 +29,15 @@ class Bookmark extends FHCAPI_Controller
parent::__construct([
'getBookmarks' => self::PERM_LOGGED,
'delete' => self::PERM_LOGGED,
'insert' => self::PERM_LOGGED,
'insert' => self::PERM_LOGGED,
'update' => self::PERM_LOGGED,
]);
'changeOrder' => self::PERM_LOGGED,
'getAllBookmarkTags' => self::PERM_LOGGED,
'getTagFilter' => self::PERM_LOGGED,
'addAndUpdateTagFilter' => self::PERM_LOGGED,
'isInOverride' => self::PERM_LOGGED,
'addWidgetToOverride' => self::PERM_LOGGED,
]);
$this->load->model('dashboard/Bookmark_model', 'BookmarkModel');
@@ -51,7 +57,7 @@ class Bookmark extends FHCAPI_Controller
*/
public function getBookmarks()
{
$this->BookmarkModel->addOrder("bookmark_id");
$this->BookmarkModel->addOrder("sort");
$bookmarks = $this->BookmarkModel->loadWhere(["uid"=>$this->uid]);
$bookmarks = $this->getDataOrTerminateWithError($bookmarks);
@@ -99,8 +105,12 @@ class Bookmark extends FHCAPI_Controller
$url = $this->input->post('url',true);
$title = $this->input->post('title',true);
$tag = $this->input->post('tag', true);
if (is_array($tag)) {
$tag = json_encode($tag); // convert PHP array to JSON string
}
$sort = $this->input->post('sort', true);
$insert_into_result = $this->BookmarkModel->insert(['uid'=>$this->uid, 'url'=>$url, 'title'=>$title,'tag'=>$tag, 'insertvon'=>$this->uid, 'updateamum'=>NULL, 'updatevon'=>NULL]);
$insert_into_result = $this->BookmarkModel->insert(['uid'=>$this->uid, 'url'=>$url, 'title'=>$title,'tag'=>$tag, 'insertvon'=>$this->uid, 'updateamum'=>NULL, 'updatevon'=>NULL, 'sort'=>$sort,]);
$insert_into_result = $this->getDataOrTerminateWithError($insert_into_result);
@@ -123,16 +133,195 @@ class Bookmark extends FHCAPI_Controller
$url = $this->input->post('url',true);
$title = $this->input->post('title',true);
$tag = $this->input->post('tag', true);
if (is_array($tag)) {
$tag = json_encode($tag);
}
$now = new DateTime();
$now = $now->format('Y-m-d H:i:s');
$update_result = $this->BookmarkModel->update($bookmark_id,['url'=>$url, 'title'=>$title,'updateamum'=>$now]);
$update_result = $this->BookmarkModel->update($bookmark_id,['url'=>$url, 'title'=>$title, 'tag'=>$tag, 'updateamum'=>$now]);
$update_result = $this->getDataOrTerminateWithError($update_result);
$this->terminateWithSuccess($update_result);
}
/**
* changes sort of two bookmarks in the bookmark table
* @access public
* @return void
*/
public function changeOrder($bookmark_id1, $bookmark_id2)
{
$result1 = $this->BookmarkModel->load($bookmark_id1);
$data1 = $this->getDataOrTerminateWithError($result1);
$sort1 = current($data1)->sort;
$result2 = $this->BookmarkModel->load(["bookmark_id"=>$bookmark_id2]);
$data2 = $this->getDataOrTerminateWithError($result2);
$sort2 = current($data2)->sort;
$update_result1 = $this->BookmarkModel->update($bookmark_id1,['sort'=>$sort2,]);
$update_result[] = $this->getDataOrTerminateWithError($update_result1);
$update_result2 = $this->BookmarkModel->update($bookmark_id2,['sort'=>$sort1,]);
$update_result[] = $this->getDataOrTerminateWithError($update_result2);
$this->terminateWithSuccess($update_result);
}
/**
* get all the bookmark tags associated to a user
* @access public
* @return void
*/
public function getAllBookmarkTags()
{
$this->BookmarkModel->addOrder("sort");
$result = $this->BookmarkModel->getAllBookmarkTags($this->uid);
$bookmarks = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(current($bookmarks));
}
/**
* get all tagFilter of a certain bookmark widget
* @access public
* @return void
*/
public function getTagFilter($widgetId, $sectionName)
{
$result = $this->BookmarkModel->getTagFilter($widgetId, $this->uid, $sectionName);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(current($data));
}
/**
* get all tagFilter of a certain bookmark widget
* @access public
* @return void
*/
public function addAndUpdateTagFilter($widgetId, $sectionName)
{
$tags = $this->input->post('tags',true);
if (is_array($tags))
{
$tags = json_encode($tags);
}
$result = $this->BookmarkModel->addAndUpdateTagFilter($widgetId, $this->uid, $sectionName, $tags);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
}
/**
* checks if a widget has already an entry in the benutzeroverride
* @access public
* @return void
*/
public function isInOverride($widgetId, $sectionName)
{
$result = $this->BookmarkModel->checkOrAddToOverride($widgetId, $this->uid, $sectionName);
$data = getData($result);
if(!$data)
$this->terminateWithSuccess([false, 0]);
$id = current($data)->widgetid;
$id = trim($id, '"');
if ($id != $widgetId)
$this->terminateWithSuccess([false, 1]);
else
$this->terminateWithSuccess([true, null]);
}
/**
* adds widget benutzeroverride
* @access public
* @return void
*/
public function addWidgetToOverride($widgetId, $sectionName, $mode, $x, $y, $h, $w)
{
$this->load->library('dashboard/DashboardLib', null, 'DashboardLib');
$dashboard_kurzbz = "CIS";
$structure = [
"custom" => [
"widgets" => []
],
"general" => [
"widgets" => [
$widgetId => [
"widget" => 3,
"config" => new stdClass(),
"place" => [
"3" => [
"x" => $x,
"y" => $y,
"w" => $w,
"h" => $h
]
],
"widgetid" => $widgetId,
"custom" => 1,
"id" => "insertByBookmarkwidget"
]
]
]
];
$jsonOverrideNew = json_encode($structure, JSON_UNESCAPED_UNICODE);
//no existing benutzeroverride
if($mode == 0)
{
$override = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $this->uid);
$override->override = $jsonOverrideNew;
$result = $this->DashboardLib->insertOrUpdateOverride($override);
$this->terminateWithSuccess($result);
}
//benutzeroverride existing, but widget not included
elseif($mode == 1)
{
$overrideExisting = $this->DashboardLib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $this->uid);
$override = json_decode($overrideExisting->override, true); // decode as Array
$newWidget = [
"widget" => 3,
"config" => new stdClass(),
"place" => [
"3" => [
"x" => $x,
"y" => $y,
"w" => $w,
"h" => $h
]
],
"widgetid" => $widgetId,
"custom" => 1,
"id" => "insertByBookmarkwidget"
];
$override['general']['widgets'][$widgetId] = $newWidget;
$jsonOverrideUpdated = json_encode($override, JSON_UNESCAPED_UNICODE);
$overrideExisting->override = $jsonOverrideUpdated;
$result = $this->DashboardLib->insertOrUpdateOverride($overrideExisting);
$this->terminateWithSuccess($result);
}
else
$this->terminateWithError("Error: no known mode");
}
}
@@ -12,7 +12,83 @@ class Bookmark_model extends DB_Model
$this->pk = 'bookmark_id';
}
/**
* returns all bookmark tags of a user
*/
public function getAllBookmarkTags($uid)
{
$qry = "
SELECT array_agg(DISTINCT tag) AS data
FROM (
SELECT jsonb_array_elements_text(tag) AS tag
FROM dashboard.tbl_bookmark
WHERE uid = ?
) t;
";
return $this->execQuery($qry, array('uid' => $uid));
}
/**
* Get Overrides of given uid and funktion_kurzbz and widget_id
* @param integer $widget_id
* @param string $uid
* @param string $funktion_kurzbz
* @return array
*/
public function getTagFilter($widgetId, $uid, $funktion_kurzbz)
{
$qry = " SELECT override -> '" . $funktion_kurzbz . "'-> 'widgets'-> '" . $widgetId . "'-> 'config' -> 'tags' AS tags FROM dashboard.tbl_dashboard_benutzer_override WHERE uid = '" . $uid . "';";
return $this->execQuery($qry);
}
/*
* updates Tagfilter
* checks and changes type of config
* if config == array -> change to object
*/
public function addAndUpdateTagFilter($widgetId, $uid, $funktion_kurzbz, $tags)
{
$params = [
$funktion_kurzbz, $widgetId,
$funktion_kurzbz, $widgetId,
$funktion_kurzbz, $widgetId,
$funktion_kurzbz, $widgetId,
$tags,
$uid
];
$qry = "
UPDATE dashboard.tbl_dashboard_benutzer_override
SET override = jsonb_set(
-- check if config is object
jsonb_set(
COALESCE(override, '{}'::jsonb),
format('{%s,widgets,%s,config}', ?, ?)::text[],
CASE
WHEN jsonb_typeof(override #> format('{%s,widgets,%s,config}', ?, ?)::text[]) = 'array'
THEN '{}'::jsonb
ELSE COALESCE((override #> format('{%s,widgets,%s,config}', ?, ?)::text[])::jsonb, '{}'::jsonb)
END,
true
),
-- insert tags
format('{%s,widgets,%s,config,tags}', ?, ?)::text[],
?::jsonb,
true
)
WHERE uid = ?
";
return $this->execQuery($qry, $params);
}
public function checkOrAddToOverride($widgetId, $uid, $funktion_kurzbz)
{
$params = [$funktion_kurzbz, $widgetId, $uid];
$qry = " SELECT override -> '" . $funktion_kurzbz . "'-> 'widgets'-> '" . $widgetId . "'-> 'widgetid' as widgetid FROM dashboard.tbl_dashboard_benutzer_override WHERE uid = '" . $uid . "';";
return $this->execQuery($qry, $params);
}
}
@@ -23,12 +23,14 @@ $includesArray = array(
'public/css/components/FormUnderline.css',
'public/css/components/abgabetool/abgabe.css',
'public/css/Cis4/Cms.css',
'public/css/Cis4/Studium.css',
'public/css/Cis4/Studium.css'
),
'customJSs' => array(
'vendor/npm-asset/primevue/accordion/accordion.min.js',
'vendor/npm-asset/primevue/accordiontab/accordiontab.min.js',
'vendor/npm-asset/primevue/checkbox/checkbox.min.js',
'vendor/npm-asset/primevue/chips/chips.min.js',
'vendor/npm-asset/primevue/multiselect/multiselect.min.js',
'vendor/npm-asset/primevue/inputnumber/inputnumber.min.js',
'vendor/npm-asset/primevue/speeddial/speeddial.min.js',
'vendor/npm-asset/primevue/textarea/textarea.min.js',
+42 -4
View File
@@ -17,6 +17,7 @@
export default {
getBookmarks() {
return {
method: 'get',
url: '/api/frontend/v1/Bookmark/getBookmarks'
@@ -28,18 +29,55 @@ export default {
url: `/api/frontend/v1/Bookmark/delete/${bookmark_id}`
};
},
update({ bookmark_id, url, title, tag=null }) {
update({ bookmark_id, url, title, tag }) {
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/update/${bookmark_id}`,
params: { url, title }
params: { url, title, tag }
};
},
insert({ url, title, tag }) {
insert({ url, title, tag, sort }) {
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/insert`,
params: { url, title, tag }
params: { url, title, tag, sort }
};
},
changeOrder(bookmark_id1, bookmark_id2) {
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/changeOrder/${bookmark_id1}/${bookmark_id2}`,
};
},
getAllBookmarkTags() {
return {
method: 'get',
url: '/api/frontend/v1/Bookmark/getAllBookmarkTags'
};
},
getTagFilter(widgetId, sectionName){
return {
method: 'get',
url: `/api/frontend/v1/Bookmark/getTagFilter/${widgetId}/${sectionName}`
};
},
addTagFilter(widgetId, sectionName, tags){
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/addAndUpdateTagFilter/${widgetId}/${sectionName}`,
params: { tags }
};
},
isInOverride(widgetId, sectionName){
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/isInOverride/${widgetId}/${sectionName}`
};
},
addWidgetToOverride(widgetId, sectionName, mode, x, y, h, w){
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/addWidgetToOverride/${widgetId}/${sectionName}/${mode}/${x}/${y}/${h}/${w}`,
};
}
};
+2 -1
View File
@@ -203,8 +203,9 @@ export default {
</div>
</Transition>
</div>
<!-- TODO Manu rename/remove-->
<div v-if="ready" class="card-body overflow-hidden p-0">
<component :is="component" v-model:shared-data="sharedData" :config="arguments" :width="width" :height="height" @setConfig="setConfig" @change="changeConfigManually"></component>
<component :is="component" v-model:shared-data="sharedData" :config="arguments" :width="width" :height="height" :widget-id="widgetID" :item_data="item_data" @setConfig="setConfig" @change="changeConfigManually"></component>
</div>
<div v-else class="card-body overflow-hidden text-center d-flex flex-column justify-content-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
<bs-modal v-if="hasConfig" ref="config" @hideBsModal="handleHideBsModal" @showBsModal="handleShowBsModal">
@@ -5,7 +5,9 @@ export default {
"height",
"configMode",
"sharedData",
"widgetInfo"
"widgetInfo",
"widgetId",
"item_data"
],
emits: [
"setConfig",
+359 -20
View File
@@ -12,23 +12,38 @@ export default {
editModeIsActive: {
type: Boolean,
default: false
},
sectionName: {
type: String,
default: false
}
},
components:{
CoreForm,
FormInput,
BsModal
BsModal,
PvChips: primevue.chips,
PvMultiSelect: primevue.multiselect,
PvAutoComplete: primevue.autocomplete,
},
data: () => ({
bookmark_id: null,
title_input: "",
url_input: "",
sort: null,
validation: {
invalidURL: false,
invalidTitel: false,
},
tagsArrayMS: [],
tagsArrayAC: [],
selectedTags: [],
newTag: null,
selectedFilters: [],
filter: [],
filteredArray: [],
sharedFiltered: {},
}),
computed: {
tagName() {
return this.config.tag !== undefined && this.config.tag.length > 0
@@ -42,6 +57,27 @@ export default {
return false;
},
newSort(){
if(this.shared.length == 0)
return 1;
else
return Math.max(...this.shared.map(b => b.sort)) + 1;
},
maxSort(){
if(this.shared.length == 0)
return 0;
else
return Math.max(...this.sharedFiltered.map(b => b.sort));
},
minSort(){
if(this.shared.length == 0)
return 0;
else
return Math.min(...this.sharedFiltered.map(b => b.sort));
},
filterInput(){
return this.selectedFilters.map(item => item.tag);
},
},
methods: {
stopDrag(event){
@@ -49,7 +85,8 @@ export default {
},
clearInputs(){
this.title_input = "";
this.url_input = "";
this.url_input = "";
this.selectedTags = [];
},
openCreateModal() {
this.$refs.createModal.show()
@@ -58,22 +95,26 @@ export default {
this.title_input = bookmark.title;
this.url_input = bookmark.url;
this.bookmark_id = bookmark.bookmark_id;
this.$refs.editModal.show()
this.selectedTags = JSON.parse(bookmark.tag);
this.$refs.editModal.show();
},
editBookmark(event){
event.preventDefault();
if(!this.bookmark_id || !this.url_input || !this.title_input) return;
this.$api
.call(ApiBookmark.update({
bookmark_id: this.bookmark_id,
title: this.title_input,
url: this.url_input,
tag: this.selectedTags,
}))
.then((res) => res.data)
.then((result) => {
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkUpdated"));
// refetch the bookmarks to see the updates
this.getAllBookmarkTags();
this.fetchBookmarks();
// reset the values for the title and url inputs
this.clearInputs();
@@ -94,16 +135,21 @@ export default {
// early return if validation failed
if (!this.isValidationSuccessfull()) return;
// get highest Sort
this.sort = this.newSort;
this.$api
.call(ApiBookmark.insert({
tag: this.config.tag,
tag: this.selectedTags,
title: this.title_input,
url: this.url_input,
sort: this.sort
}))
.then((res) => res.data)
.then((result) => {
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkAdded"));
// refetch the bookmarks to see the updates
this.getAllBookmarkTags();
this.fetchBookmarks();
this.$refs.createModal.hide();
// reset the values for the title and url inputs
@@ -111,7 +157,6 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
},
isValidationSuccessfull() {
// validate the input fields
if (this.title_input.length === 0) {
@@ -131,6 +176,10 @@ export default {
.then((res) => res.data)
.then((result) => {
this.shared = result;
this.$nextTick(() => {
this.sharedFiltered = this.filterBookmarksByTags(this.shared);
});
})
.catch(this.$fhcAlert.handleSystemError);
},
@@ -147,39 +196,268 @@ export default {
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkDeleted"));
// refetch the bookmarks to see the updates
this.fetchBookmarks();
this.getAllBookmarkTags();
})
.catch(this.$fhcAlert.handleSystemError);
},
filterBookmarksByTags(bookmarks) {
const filter = this.filter;
if (!filter || filter.length === 0 || filter == "[]") return bookmarks;
return bookmarks.filter(b => {
const tags = JSON.parse(b.tag || "[]");
return tags.some(tag => filter.includes(tag));
});
},
sortDown(bookmark_id){
const current = this.sharedFiltered.find(b => b.bookmark_id === bookmark_id);
const next = this.sharedFiltered
.filter(b => b.sort > current.sort)
.sort((a, b) => a.sort - b.sort)[0];
if (!next) {
console.log("lowest sort item, no change");
return;
}
this.changeOrder(current.bookmark_id, next.bookmark_id);
},
sortUp(bookmark_id){
const current = this.sharedFiltered.find(b => b.bookmark_id === bookmark_id);
const next = this.sharedFiltered
.filter(b => b.sort < current.sort)
.sort((a, b) => a.sort + b.sort)[0];
if (!next) {
console.log("highest sort item, no change");
return;
}
this.changeOrder(current.bookmark_id, next.bookmark_id);
},
addNewTag(){
if(this.newTag != null && this.newTag.length) {
this.tagsArrayMS.push({tag: this.newTag, code: this.newTag});
this.selectedTags.push({tag: this.newTag, code: this.newTag});
this.newTag = null;
}
else
this.$fhcAlert.alertError(this.$p.t("bookmark", "errorInputNecessary"));
},
changeOrder(bookmark_id_1, bookmark_id_2){
this.$api
.call(ApiBookmark.changeOrder(bookmark_id_1, bookmark_id_2))
.then((res) => res.data)
.then((result) => {
// refetch the bookmarks to see the updates
this.fetchBookmarks();
})
.catch(this.$fhcAlert.handleSystemError);
},
hasTags(link) {
if (!link || !link.tag) return false;
let tags = link.tag;
if (typeof tags === 'string') {
try {
tags = JSON.parse(tags)
} catch {
return false;
}
}
if (Array.isArray(tags) && tags.length > 0) {
return tags.join(' ');
}
},
prepareTag(bookmarkArr){
const parsedArr = Array.isArray(bookmarkArr)
? bookmarkArr
: JSON.parse(bookmarkArr);
const result = parsedArr.map(tag => ({
tag,
code: tag
}));
return result;
},
getAllBookmarkTags(){
this.$api
.call(ApiBookmark.getAllBookmarkTags())
.then((res) => res.data)
.then((result) => {
//Version Chips
this.tagsArrayMS = this.prepareTag(result.data);
//Version Autocomplete
this.tagsArrayAC = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
openFilterModal(){
this.$refs.filterModal.show();
},
async handleAddingTagFilter(widgetId){
const result = await this.isInOverride(widgetId);
if (!result) {
return;
}
const [status, reason] = result;
if (status) {
this.addTagFilter(widgetId);
} else {
this.addWidgetToOverride(widgetId, reason);
}
},
addTagFilter(widgetId){
this.$api
.call(ApiBookmark.addTagFilter(widgetId, this.sectionName, this.filterInput))
.then((res) => res.data)
.then((result) => {
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "filterUpdated"));
this.$refs.filterModal.hide();
this.getTagFilter(this.widgetId);
this.$nextTick(() => {
this.getAllBookmarkTags();
});
this.fetchBookmarks();
})
.catch(this.$fhcAlert.handleSystemError);
},
async isInOverride(widgetId) {
try {
const res = await this.$api.call(ApiBookmark.isInOverride(widgetId, this.sectionName));
const result = res.data;
return result;
} catch (err) {
this.$fhcAlert.handleSystemError(err);
return null;
}
},
addWidgetToOverride(widgetId, reason){
this.$api
.call(ApiBookmark.addWidgetToOverride(
widgetId,
this.sectionName,
reason,
this.item_data.x,
this.item_data.y,
this.item_data.h,
this.item_data.w
))
.then((res) => res.data)
.then((result) => {
this.addTagFilter(widgetId);
})
.catch(this.$fhcAlert.handleSystemError);
},
getTagFilter(widget_id){
this.$api
.call(ApiBookmark.getTagFilter(widget_id, this.sectionName))
.then((res) => res.data)
.then((result) => {
const rawTags = result.tags; // string
this.filter = rawTags;
if(rawTags != null) {
this.rawTagsParsed = JSON.parse(rawTags);
this.selectedFilters = this.prepareTag(this.rawTagsParsed);
}
this.fetchBookmarks();
})
.catch(this.$fhcAlert.handleSystemError);
},
search(event) {
const query = event.query ?? "";
// Filter for text
this.filteredArray = this.tagsArrayAC.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
// input if search not successful
if (this.filteredArray.length === 0 && query) {
this.filteredArray = [query];
}
},
},
async mounted() {
if(this.widgetId) {
this.getTagFilter(this.widgetId);
}
await this.fetchBookmarks();
this.getAllBookmarkTags();
},
created() {
//
// this.$emit('setConfig', true); -> use this to enable widget config mode if needed
// this.$emit('setConfig', true); // -> use this to enable widget config mode if needed
},
template: /*html*/ `
<div class="widgets-url w-100 h-100 overflow-auto" style="padding: 1rem 1rem;">
<div class="d-flex flex-column justify-content-between">
<button class="btn btn-outline-secondary btn-sm w-100 mt-2 card" @click="openCreateModal" type="button">{{$p.t('bookmark','newLink')}}</button>
<template v-if="shared">
<div class="d-flex mt-2">
<button class="btn btn-outline-secondary btn-sm flex-grow-1 me-2" @click="openCreateModal">
{{$p.t('bookmark','newLink')}}
</button>
<button v-if="selectedFilters.length" class="btn btn-secondary btn-sm" :title="this.$p.t('bookmark/editFilter')" @click="openFilterModal">
<i class="fa-solid fa-filter-circle-xmark"></i>
</button>
<button v-else class="btn btn-outline-secondary btn-sm" :title="this.$p.t('bookmark/filterByTags')" @click="openFilterModal">
<i class="fa-solid fa-filter"></i>
</button>
</div>
<template v-if="sharedFiltered">
<template v-if="!emptyBookmarks">
<div v-for="link in shared" :key="link.id" class="d-flex mt-2">
<a target="_blank" :href="link.url">
<div v-for="link in sharedFiltered" :key="link.id" class="d-flex mt-2">
<a target="_blank" :href="link.url" class="me-1">
<i class="fa fa-solid fa-arrow-up-right-from-square me-1"></i>{{ link.title }}
</a>
<span
v-if="hasTags(link)"
:title="hasTags(link)"
style="color: silver;"
>
<i class="fa fa-solid fa-tag text-gray-500" aria-hidden="true"></i>
</span>
<div class="ms-auto">
<!--EDIT BOOKMARK-->
<a type="button" href="#" @click.prevent="openEditModal(link)" aria-label="edit bookmark" v-tooltip="{showDelay:1000,value:'edit bookmark'}">
<a type="button" href="#" @click.prevent="openEditModal(link)" aria-label="edit bookmark" :title="this.$p.t('bookmark/editBookmark')">
<i class="fa fa-edit me-1" aria-hidden="true"></i>
</a>
<!--DELETE BOOKMARK-->
<a type="button" id="deleteBookmark" href="#" aria-label="delete bookmark" v-tooltip="{showDelay:1000,value:'delete bookmark'}" @click.prevent="removeLink(link.bookmark_id)">
<a type="button" id="deleteBookmark" href="#" aria-label="delete bookmark" :title="this.$p.t('bookmark/deleteBookmark')" @click.prevent="removeLink(link.bookmark_id)">
<i class="fa fa-regular fa-trash-can" aria-hidden="true"></i>
</a>
<!--SORT BOOKMARKS-->
<a
v-if="sharedFiltered.length > 1"
type="button"
id="downsortBookmark"
href="#"
aria-label="sortdown bookmark"
:title="this.$p.t('bookmark/sortDownwards')"
@click.prevent="sortDown(link.bookmark_id)"
>
<i :class="[ 'fa', 'fa-arrow-down', 'me-1', link.sort === maxSort ? 'text-light pointer-events-none' : '' ]"></i>
</a>
<a
v-if="sharedFiltered.length > 1"
type="button"
id="upsortBookmark"
href="#"
aria-label="sortup bookmark"
:title="this.$p.t('bookmark/sortToTop')"
@click.prevent="sortUp(link.bookmark_id)"
>
<i :class="[ 'fa', 'fa-arrow-up', 'me-1', link.sort === minSort ? 'text-light pointer-events-none' : '' ]"></i>
</a>
</div>
</div>
</template>
@@ -188,15 +466,15 @@ export default {
<span>{{$p.t('bookmark','emptyBookmarks')}}</span>
</div>
</template>
<template v-else>
<p v-for="i in 4" class="placeholder-glow">
<span class="placeholder" :class="{'col-9' : true}"></span>
</p>
</template>
</div>
</div>
</template>
</div>
<!--EDIT MODAL-->
<teleport to="body">
<bs-modal @[\`hide.bs.modal\`]="bookmark_id=null; clearInputs();" ref="editModal">
@@ -204,8 +482,20 @@ export default {
<h2>{{$p.t('bookmark','editLink')}}</h2>
</template>
<template #default>
<form-input :label="$p.t('profil','Titel')" :title="$p.t('profil','Titel')" id="editTitle" v-model="title_input" name="title" class="mb-2"></form-input>
<form-input :label="$p.t('profil','Titel')" :title="$p.t('profil','Titel')" id="editTitle" v-model="title_input" name="title" class="mb-2"></form-input>
<form-input label="Url" title="Url" id="editUrl" v-model="url_input" name="url"></form-input>
<label class="mt-2">Tags</label>
<div class="mt-2">
<PvAutoComplete
v-model="selectedTags"
multiple
dropdown
:suggestions="filteredArray"
@complete="search"
/>
</div>
</template>
<template #footer>
<button @click="editBookmark" class="btn btn-primary">{{$p.t('bookmark','saveLink')}}</button>
@@ -219,17 +509,66 @@ export default {
<h2>{{$p.t('bookmark','newLink')}}</h2>
</template>
<template #default>
<form-input :label="$p.t('profil','Titel')" :title="$p.t('profil','Titel')" id="insertTitle" v-model="title_input" name="title" class="mb-2"></form-input>
<form-input label="Url" title="Url" id="insertUrl" v-model="url_input" name="url"></form-input>
<label class="mt-2">Tags</label>
<div class="mt-2">
<PvAutoComplete
v-model="selectedTags"
multiple
dropdown
:suggestions="filteredArray"
@complete="search"
/>
</div>
</template>
<template #footer>
<button @click="insertBookmark" class="btn btn-primary">{{$p.t('bookmark','saveLink')}}</button>
</template>
</bs-modal>
</teleport>
<!--FILTER MODAL-->
<teleport to="body">
<bs-modal @[\`hide.bs.modal\`]="clearInputs();" ref="filterModal">
<template #title>
<h2>{{$p.t('bookmark','headerFilterBookmark')}}</h2>
</template>
<template #default>
<div class="mt-2 row">
<div class="col-10">
<PvMultiSelect
v-model="selectedFilters"
id="tagFilterUrl"
:options="tagsArrayMS"
optionLabel="tag"
display="chip"
:placeholder="$p.t('bookmark','noFilter')"
:maxSelectedLabels="3"
class="p-inputtext-sm w-100 me-2"
/>
</div>
</div>
</template>
<template #footer>
<button
class="btn btn-secondary"
@click="handleAddingTagFilter(widgetId)"
:title="$p.t('bookmark','filterByTags')"
>
OK
</button>
</template>
</bs-modal>
</teleport>
`,
};
/*
Link JSON structure:
{
+2 -1
View File
@@ -95,6 +95,7 @@ require_once('dbupdate_3.4/71645_studvw_messagetab_ladezeit.php');
require_once('dbupdate_3.4/71566_studienordnungsdokument_neuer_organisationseinheitstyp_programm.php');
require_once('dbupdate_3.4/70376_lohnguide.php');
require_once('dbupdate_3.4/76150_perm_other_lv_plan.php');
require_once('dbupdate_3.4/68957_dashboard_bookmark_neue_Spalte_sort.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';
@@ -468,7 +469,7 @@ $tabellen=array(
"wawi.tbl_rechnungsbetrag" => array("rechnungsbetrag_id","rechnung_id","mwst","betrag","bezeichnung","ext_id"),
"wawi.tbl_aufteilung" => array("aufteilung_id","bestellung_id","oe_kurzbz","anteil","insertamum","insertvon","updateamum","updatevon"),
"wawi.tbl_aufteilung_default" => array("aufteilung_id","kostenstelle_id","oe_kurzbz","anteil","insertamum","insertvon","updateamum","updatevon"),
"dashboard.tbl_bookmark" => array("bookmark_id","uid","url","title","tag","insertamum","insertvon","updateamum","updatevon"),
"dashboard.tbl_bookmark" => array("bookmark_id","uid","url","title","tag","insertamum","insertvon","updateamum","updatevon","sort"),
);
@@ -0,0 +1,57 @@
<?php
if (! defined('DB_NAME')) exit('No direct script access allowed');
//Add column sort to dashboard.tbl_bookmark
if(!@$db->db_query("SELECT sort FROM dashboard.tbl_bookmark LIMIT 1")) {
$qry = "ALTER TABLE dashboard.tbl_bookmark ADD COLUMN sort integer DEFAULT NULL;
COMMENT ON COLUMN dashboard.tbl_bookmark.sort IS 'Sort Index for Bookmark.';
";
if (!$db->db_query($qry))
echo '<strong>dashboard.tbl_bookmark ' . $db->db_last_error() . '</strong><br>';
else
echo '<br>Spalte sort zu Tabelle dashboard.tbl_bookmark hinzugefügt';
//add preliminary Sort for all bookmarks if NULL
if(@$db->db_query("SELECT sort FROM dashboard.tbl_bookmark LIMIT 1")) {
$qry = "WITH ranked AS (
SELECT
t1.bookmark_id,
(
SELECT COUNT(*)
FROM dashboard.tbl_bookmark t2
WHERE t2.uid = t1.uid
AND t2.bookmark_id <= t1.bookmark_id
) AS rn
FROM dashboard.tbl_bookmark t1
)
UPDATE dashboard.tbl_bookmark t
SET sort = ranked.rn
FROM ranked
WHERE t.bookmark_id = ranked.bookmark_id
AND t.sort IS NULL;";
if (!$db->db_query($qry))
echo '<strong>dashboard.tbl_bookmark ' . $db->db_last_error() . '</strong><br>';
else
echo '<br>Tabelle dashboard.tbl_bookmark: Spalte sort mit vorläufiger Sortierung befüllt';
}
}
//set column tag to type JSONB
if($result = @$db->db_query("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='dashboard'
AND TABLE_NAME='tbl_bookmark' AND COLUMN_NAME = 'tag'
AND DATA_TYPE='character varying' AND character_maximum_length='255';"))
{
$qry = "
ALTER TABLE dashboard.tbl_bookmark
ALTER COLUMN tag TYPE jsonb
USING tag::jsonb;
";
if (!$db->db_query($qry))
echo '<strong>dashboard.tbl_bookmark ' . $db->db_last_error() . '</strong><br>';
else
echo '<br>Tabelle dashboard.tbl_bookmark: Spalte tag auf Typ JSONB geändert';
}
+201
View File
@@ -35742,6 +35742,207 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'editFilter',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Filter bearbeiten',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'edit Filter',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'filterByTags',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Nach Tags Filtern',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'filter by Tags',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'sortDownwards',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Nach unten verschieben',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'sort downwards',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'sortToTop',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Nach oben verschieben',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'sort to top',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'deleteBookmark',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lesezeichen löschen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'delete Bookmark',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'editBookmark',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lesezeichen bearbeiten',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'edit Bookmark',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'filterUpdated',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Filteraktion erfolgreich durchgeführt',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'filter updated successfully',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'errorInputNecessary',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Eingabe eines Zeichens erforderlich',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'One character must be entered',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'noFilter',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Kein Filter für Lesezeichen ausgewählt',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'No bookmark filter entered',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'bookmark',
'phrase' => 'headerFilterBookmark',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lesezeichen filtern',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Filter Bookmarks',
'description' => '',
'insertvon' => 'system'
)
)
),
// BOOKMARK PHRASEN END ----------------------------------------------------------------------
array(
'app' => 'core',
'category' => 'global',