Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5176c77
Initial plan
Copilot Jun 1, 2026
5eff18e
Implement eportfolio card redesign for folder/category views
Copilot Jun 1, 2026
9e568bd
Finalize card redesign validation updates
Copilot Jun 1, 2026
13d2dc4
refactor: replace PHP string-concat card rendering with Mustache temp…
Copilot Jun 1, 2026
f31b421
Fix FA free icons and inline fixed icons in mustache templates
Copilot Jun 2, 2026
32cb4f0
Revert "Fix FA free icons and inline fixed icons in mustache templates"
richardwolfmayr Jun 2, 2026
6a8f227
fix pro to free fontawesome
richardwolfmayr Jun 2, 2026
942c5b5
Fix: make category card-footer consistent height with item card-footer
Copilot Jun 2, 2026
c129a4f
Refactor: extract output classes and move utility functions to lib.php
Copilot Jun 2, 2026
b565efe
Remove redundant render_* methods from renderer.php
Copilot Jun 2, 2026
83269ce
Move category helper functions to classes/category_helper.php
Copilot Jun 2, 2026
4bb969c
Reduce duplication in artefact card output classes via shared base class
Copilot Jun 2, 2026
7f498a1
Fix variable naming style in category_helper.php
Copilot Jun 2, 2026
da8f172
rename templates to match output class names
Copilot Jun 2, 2026
2c088e0
cleanup
richardwolfmayr Jun 2, 2026
8e63c49
unify artefact card design for folder and flat views
Copilot Jun 2, 2026
ce58941
fix: uniform card height via min-height on column wrappers
Copilot Jun 2, 2026
3a9d0b0
Revert "fix: uniform card height via min-height on column wrappers"
richardwolfmayr Jun 2, 2026
d743bc4
fix: separate text and chips zones with fixed card height and overflo…
Copilot Jun 2, 2026
ce5bdad
Show item categories as badges in shared_item.php detail view
Copilot Jun 2, 2026
8823cf1
Show only last category segment in badges, full path on hover
Copilot Jun 2, 2026
8450943
Use Bootstrap tooltip for category badges and fix cursor style
Copilot Jun 2, 2026
4982c64
Fix variable name collision: rename $fullpath result variable to $label
Copilot Jun 2, 2026
7ff0953
Always show tooltip on category chips; start fade-out earlier (70%)
Copilot Jun 2, 2026
4b39ce6
Always show tooltip on category chips in view_items.php
Copilot Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions classes/category_helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php
// This file is part of Exabis Eportfolio (extension for Moodle)
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
// (c) 2016 GTN - Global Training Network GmbH <office@gtn-solutions.com>.

namespace block_exaport;

defined('MOODLE_INTERNAL') || die();

class category_helper {

/**
* Build a category tree keyed by parent id.
*
* @param array $categories
* @return array
*/
public static function build_by_parent(array $categories): array {
$categoriesbyparent = [];
foreach ($categories as $category) {
if (!isset($categoriesbyparent[$category->pid])) {
$categoriesbyparent[$category->pid] = [];
}
$categoriesbyparent[$category->pid][] = $category;
}

return $categoriesbyparent;
}

/**
* Build the full hierarchical path name for a category, e.g. "haustiere / hunde".
*
* @param int $categoryid The category id.
* @param array $categories Associative array of all categories keyed by id (must have ->name and ->pid).
* @return string The full path name with " / " separators.
*/
public static function full_path_name(int $categoryid, array $categories): string {
$parts = [];
$id = $categoryid;
$visited = [];
while ($id && isset($categories[$id])) {
if (isset($visited[$id])) {
break; // Prevent infinite loop on circular references.
}
$visited[$id] = true;
$parts[] = $categories[$id]->name;
$id = $categories[$id]->pid ?? 0;
}
$parts = array_reverse($parts);
return implode(' / ', $parts);
}

/**
* Load all items for flat mode and attach flatcategories to each item.
*
* @param int $userid The user whose items to load.
* @param array $categories All categories keyed by id (for path name resolution).
* @param string $sqlsort SQL ORDER BY clause.
* @param array|null $allowedcategoryids Category filter behavior:
* - null: load all categories for the viewed user in flat mode; this is all categories for your own items, or only that
* other user's own categories when viewing someone else's items.
* - empty array: return no items.
* - non-empty array: only include these category IDs and remove items with no matching categories.
* @return array The items array with ->flatcategories populated.
*/
public static function load_flat_items(int $userid, array $categories, string $sqlsort,
?array $allowedcategoryids = null): array {
global $DB, $USER;

if ($allowedcategoryids !== null && empty($allowedcategoryids)) {
// Keep the shared flat-mode behavior while avoiding an empty IN() SQL clause.
return [];
}
if ($allowedcategoryids !== null) {
$items = block_exaport_get_items_by_category_and_user(0, $allowedcategoryids, $sqlsort, true);
} else {
// this gets ALL the items of that user... e.g. unshared ones as well. As a teacher, this loads all the students items
// but they get filtered a few lines later with the unset()
$items = block_exaport_get_items_by_category_and_user($userid, null, $sqlsort);
}

if (!$items) {
return [];
}

$itemids = array_keys($items);
[$iteminsql, $iteminparams] = $DB->get_in_or_equal($itemids, SQL_PARAMS_QM);

// Belt-and-suspenders: restrict to the viewed user's own categories,
// even though items are already scoped by userid.
$is_viewing_other_user = $allowedcategoryids === null && (int)$userid !== (int)$USER->id;

$sql = "SELECT ic.id AS icid, ic.itemid, c.id, c.name, c.pid
FROM {block_exaportitemcate} ic
JOIN {block_exaportcate} c ON c.id = ic.cateid
WHERE ic.itemid $iteminsql";
$params = $iteminparams;

// Belt-and-suspenders: restrict to the viewed user's own categories,
// even though items are already scoped by userid.
if ($is_viewing_other_user) {
$sql .= " AND c.userid = ?";
$params[] = $userid;
}

if ($allowedcategoryids !== null) {
[$catinsql, $catinparams] = $DB->get_in_or_equal($allowedcategoryids, SQL_PARAMS_QM);
$sql .= " AND c.id $catinsql";
$params = array_merge($params, $catinparams);
}

$sql .= " ORDER BY c.name ASC";
$itemcategories = $DB->get_records_sql($sql, $params);

$categoriesbyitem = [];
foreach ($itemcategories as $itemcategory) {
$itemcategory->name = self::full_path_name($itemcategory->id, $categories);
if (!isset($categoriesbyitem[$itemcategory->itemid])) {
$categoriesbyitem[$itemcategory->itemid] = [];
}
$categoriesbyitem[$itemcategory->itemid][] = $itemcategory;
}

foreach ($items as $itemid => $item) {
$item->flatcategories = $categoriesbyitem[$item->id] ?? [];
if ($allowedcategoryids !== null && !$item->flatcategories) {
unset($items[$itemid]); // this is crucial! This unsets all items that should not be displayed
}
}

return $items;
}
}
114 changes: 114 additions & 0 deletions classes/output/artefact_card.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
// This file is part of Exabis Eportfolio (extension for Moodle)
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
// (c) 2016 GTN - Global Training Network GmbH <office@gtn-solutions.com>.

namespace block_exaport\output;

defined('MOODLE_INTERNAL') || die();

use renderable;
use renderer_base;
use templatable;

/**
* Abstract base class for artefact card output objects (Bootstrap layout).
*
* Holds the shared constructor, properties, and data that is common to
* both the flat/grid card and the folder-navigation card.
*/
abstract class artefact_card implements renderable, templatable {

/** @var \stdClass $item */
protected $item;

/** @var int $courseid */
protected $courseid;

/** @var string $type */
protected $type;

/** @var int $categoryid */
protected $categoryid;

/** @var \stdClass $currentcategory */
protected $currentcategory;

/**
* Constructor.
*
* @param \stdClass $item The artefact/item record.
* @param int $courseid The course id.
* @param string $type Access type, e.g. 'mine' or 'shared'.
* @param int $categoryid The current category id (used for delete URL).
* @param \stdClass $currentcategory The currently active category.
*/
public function __construct(\stdClass $item, int $courseid, string $type, int $categoryid,
\stdClass $currentcategory) {
$this->item = $item;
$this->courseid = $courseid;
$this->type = $type;
$this->categoryid = $categoryid;
$this->currentcategory = $currentcategory;
}

/**
* Return the data fields shared by all artefact card variants.
*
* @return array
*/
protected function base_export_data(): array {
global $CFG, $USER;

$item = $this->item;
$courseid = $this->courseid;
$type = $this->type;
$categoryid = $this->categoryid;

$url = $CFG->wwwroot . '/blocks/exaport/shared_item.php?courseid=' . $courseid
. '&access=portfolio/id/' . $item->userid . '&itemid=' . $item->id;

// Build category IDs for client-side filtering.
$itemcatids = [];
if (!empty($item->flatcategories) && is_array($item->flatcategories)) {
foreach ($item->flatcategories as $cat) {
$itemcatids[] = (int)$cat->id;
}
}

$cattype = ($type == 'shared') ? '&cattype=shared' : '';
$isownitem = ($item->userid == $USER->id);
$commentcount = (int)($item->comments ?? 0);

return [
'itemnamelower' => strtolower($item->name),
'catids' => implode(',', $itemcatids),
'timemodified' => (int)$item->timemodified,
'itemid' => (int)$item->id,
'url' => $url,
'itemname' => $item->name,
'isownitem' => $isownitem,
'editurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid
. '&id=' . $item->id . '&action=edit' . $cattype,
'editicon' => block_exaport_fontawesome_icon('pen-to-square', 'regular', 1),
'deleteurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid
. '&id=' . $item->id . '&action=delete&categoryid=' . $categoryid . $cattype,
'deleteicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']),
'hascomments' => $commentcount > 0,
'commentcount' => $commentcount,
'dateformatted' => date('d.m.Y H:i', $item->timemodified),
];
}
}
43 changes: 43 additions & 0 deletions classes/output/artefact_card_flat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
// This file is part of Exabis Eportfolio (extension for Moodle)
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
// (c) 2016 GTN - Global Training Network GmbH <office@gtn-solutions.com>.

namespace block_exaport\output;

defined('MOODLE_INTERNAL') || die();

use renderer_base;

/**
* Output class for the artefact card in flat/grid mode (Bootstrap layout).
*
* Extends artefact_card_folder and adds category badge chips shown only in flat mode.
* Rendered via block_exaport/artefact_card_folder template (see renderer.php).
*/
class artefact_card_flat extends artefact_card_folder {

/**
* Export the data required by the mustache template.
*
* @param renderer_base $output
* @return array
*/
public function export_for_template(renderer_base $output): array {
return parent::export_for_template($output) + [
'categorybadges' => block_exaport_render_item_category_badges($this->item),
];
}
}
72 changes: 72 additions & 0 deletions classes/output/artefact_card_folder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
// This file is part of Exabis Eportfolio (extension for Moodle)
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
// (c) 2016 GTN - Global Training Network GmbH <office@gtn-solutions.com>.

namespace block_exaport\output;

defined('MOODLE_INTERNAL') || die();

use context_user;
use renderer_base;

/**
* Output class for the artefact card in folder-navigation mode (Bootstrap layout).
*
* Renders block_exaport/artefact_card_folder.
*/
class artefact_card_folder extends artefact_card {

/**
* Export the data required by the mustache template.
*
* @param renderer_base $output
* @return array
*/
public function export_for_template(renderer_base $output): array {
$item = $this->item;
$iconTypeProps = block_exaport_item_icon_type_options($item->type);
$typelabel = get_string($item->type, 'block_exaport');

$introtext = '';
if (!empty($item->intro)) {
$intro = file_rewrite_pluginfile_urls($item->intro, 'pluginfile.php',
context_user::instance($item->userid)->id,
'block_exaport', 'item_content',
'portfolio/id/' . $item->userid . '/itemid/' . $item->id);
$introtext = shorten_text(trim(strip_tags($intro)), 140, true);
}

$base = $this->base_export_data();
$commentcount = $base['commentcount'];
$commentlabel = $commentcount . ' ' . block_exaport_get_string($commentcount === 1 ? 'comment' : 'comments');

return $base + [
'typeicon' => '<i class="icon fa fa-' . s($iconTypeProps['iconName']) . ' fa-fw me-1"'
. ' data-bs-toggle="tooltip" data-bs-placement="top"'
. ' data-bs-title="' . s($typelabel) . '"></i>',
'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'solid', 1),
'viewlabel' => block_exaport_get_string('view'),
'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1),
'canedit' => $base['isownitem'],
'editlabel' => block_exaport_get_string('edit'),
'candelete' => $base['isownitem'] && block_exaport_item_is_editable($item->id),
'deletelabel' => block_exaport_get_string('delete'),
'introtext' => $introtext,
'compbadge' => block_exaport_get_item_comp_footer_badge($item),
'commentlabel' => $commentlabel,
];
}
}
Loading