From 5176c7702bb8740570971b043a1321fe62f68d7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:44:34 +0000 Subject: [PATCH 01/27] Initial plan From 5eff18ecd977f670b3a057713a7d9cb5976abf6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:51:42 +0000 Subject: [PATCH 02/27] Implement eportfolio card redesign for folder/category views --- css/eportfolio-cards.css | 153 ++++++++++++++++++++++++ lib/lib.php | 1 + view_items.php | 245 ++++++++++++++++++++++++++------------- 3 files changed, 317 insertions(+), 82 deletions(-) create mode 100644 css/eportfolio-cards.css diff --git a/css/eportfolio-cards.css b/css/eportfolio-cards.css new file mode 100644 index 00000000..315b6d4c --- /dev/null +++ b/css/eportfolio-cards.css @@ -0,0 +1,153 @@ +#exaport .tertiary-exabis-navigation { + padding-bottom: 25px; +} +#exaport .tertiary-exabis-navigation .navigation { + border-bottom: 1px solid var(--bs-border-color); + background-color: #fff; + margin: 0 -0.5rem; + padding: 0 0.5rem; +} +#exaport .exaport-portfolio-toolbar .input-group, +#exaport .exaport-portfolio-toolbar .form-select { + width: 100%; +} +#exaport .exaport-portfolio-toolbar .icon { + margin-right: 0; +} +#exaport .card-eportfolio .card-top { + padding: 1rem 1rem 0 1rem; +} +#exaport .card-eportfolio .card-title { + line-height: 1.4; +} +#exaport .card-eportfolio .card-title a:hover, +#exaport .card-eportfolio .card-title a:focus { + text-decoration: none; + outline: 0 !important; + box-shadow: none !important; +} +#exaport .card-eportfolio .card-title .icon { + color: #000; + color: rgba(0, 0, 0, 0.796); +} +#exaport .card-eportfolio .card-body { + padding: .5rem 1rem 1rem 1rem; +} +#exaport .card-eportfolio .eportfolio-categories { + font-size: .8rem; +} +#exaport .card-eportfolio .eportfolio-categories .badge { + font-size: .72rem; + background-color: #e9ecef !important; +} +#exaport .card-eportfolio .eportoflio-share .icon { + margin-right: 0; +} +#exaport .card-eportfolio .eportoflio-share .icon.icon-shared, +#exaport .card-eportfolio .eportoflio-comment .icon.icon-comment { + margin-right: .2rem; +} +#exaport .card-eportfolio .eportoflio-date, +#exaport .card-eportfolio .eportfolio-share-count, +#exaport .card-eportfolio .eportfolio-comment-count { + font-size: .8rem; +} +#exaport .eportfolio-card-more .dropdown-toggle { + color: #000; +} +#exaport .eportfolio-card-more .dropdown-toggle:after { + content: ""; + display: none; +} +#exaport .col-card-collection-group { + position: relative; +} +#exaport .card-collection-outer { + position: relative; +} +#exaport .card-eportfolio { + position: relative; + z-index: 3; + background: #fff; + border: .0625rem solid rgba(0,0,0,.175); + border-radius: .5rem; + padding-left: 0 !important; + padding-right: 0 !important; +} +#exaport .card-stack-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + border: .0625rem solid rgba(0,0,0,.175); + border-radius: .5rem; + pointer-events: none; +} +#exaport .card-stack-layer-1 { + top: -.2rem; + left: .2rem; + z-index: 2; +} +#exaport .card-stack-layer-2 { + top: -.4rem; + left: .4rem; + z-index: 1; + box-shadow: 2px 2px 4px rgba(0,0,0,.15); +} +#exaport .col-card-folder .card-eportfolio { + position: relative; + border-radius: 0 0.75rem 0.75rem 0rem; + margin-top: 1.2rem; +} +#exaport .col-card-folder .card-eportfolio::before { + content: ""; + position: absolute; + top: -1.15rem; + left: -1px; + width: 8rem; + height: 1.2rem; + background: var(--bs-card-bg, #fff); + border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0,0,0,.175)); + border-radius: 0.75rem 0.75rem 0 0; + background-color: #efefef; + border-bottom: 0; +} +#exaport .col-card-folder .card-eportfolio::after { + content: ""; + position: absolute; + top: -1.15rem; + left: 6.7rem; + width: 2.2rem; + height: 1.2rem; + background: var(--bs-card-bg, #fff); + border-top: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0,0,0,.175)); + border-right: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0,0,0,.175)); + transform: skewX(35deg); + transform-origin: left bottom; + border-radius: 0 0.45rem 0 0; + background-color: #efefef; +} +#exaport .col-card-folder .card-eportfolio { + height: calc(100% - 1.2rem) !important; +} +.tooltip .tooltip-inner { + text-align: left; +} +.tooltiplist { + margin: 0; + padding-left: 1rem; +} +.tooltiplist li { + margin: 0; + padding: 0; +} +#exaport .exaport-category-view .badge { + font-size: 0.70rem; + padding: 0.35rem 0.65rem; + border-radius: 0.7rem; +} +#exaport .eportfolio-categories .badge { + font-size: .78rem; +} diff --git a/lib/lib.php b/lib/lib.php index 303facfb..9e4391fc 100644 --- a/lib/lib.php +++ b/lib/lib.php @@ -339,6 +339,7 @@ function block_exaport_init_js_css() { $PAGE->requires->js('/blocks/exaport/javascript/exaport.js', true); $PAGE->requires->css('/blocks/exaport/css/styles.css'); + $PAGE->requires->css('/blocks/exaport/css/eportfolio-cards.css'); $scriptname = preg_replace('!\.[^\.]+$!', '', basename($_SERVER['PHP_SELF'])); if (file_exists($CFG->dirroot . '/blocks/exaport/css/' . $scriptname . '.css')) { diff --git a/view_items.php b/view_items.php index 6710e7df..54c24615 100644 --- a/view_items.php +++ b/view_items.php @@ -616,6 +616,16 @@ function block_exaport_print_category_select($categoriesbyparent, $currentcatego echo ''; $PAGE->requires->js_call_amd('block_exaport/view_items_state', 'init', [$folderlayout, $layout]); +$PAGE->requires->js_amd_inline(' + document.addEventListener("DOMContentLoaded", function () { + if (typeof bootstrap === "undefined" || !bootstrap.Tooltip) { + return; + } + document.querySelectorAll("[data-bs-toggle=\'tooltip\']").forEach(function (el) { + bootstrap.Tooltip.getOrCreateInstance(el); + }); + }); +'); if ($layout == 'folder') { echo '
'; @@ -918,7 +928,7 @@ function block_exaport_print_category_select($categoriesbyparent, $currentcatego } foreach ($items as $item) { - echo block_exaport_artefact_list_item($item, $courseid, $type, $categoryid, $currentcategory); + echo block_exaport_artefact_list_item($item, $courseid, $type, $categoryid, $currentcategory, ($layout == 'folder')); } echo '
'; @@ -967,6 +977,49 @@ function block_exaport_get_item_comp_icon($item) { . ''; } +/** + * Renders the competencies footer badge for Bootstrap card mode. + * + * @param stdClass $item + * @return string + */ +function block_exaport_get_item_comp_footer_badge($item) { + if (!block_exaport_check_competence_interaction()) { + return ''; + } + + $comps = block_exaport_get_active_comps_for_item($item); + if (!$comps) { + return ''; + } + + $titles = []; + foreach (['descriptors', 'topics'] as $key) { + if (!empty($comps[$key]) && is_array($comps[$key])) { + foreach ($comps[$key] as $comp) { + if (!empty($comp->title)) { + $titles[] = $comp->title; + } + } + } + } + + if (!$titles) { + return ''; + } + + $items = ''; + foreach ($titles as $title) { + $items .= html_writer::tag('li', format_string($title)); + } + $tooltiphtml = html_writer::tag('ul', $items, ['class' => 'tooltiplist']); + + return '' + . '' + . '' . count($titles) . '' + . ''; +} + /** * Prints the unified "Create" dropdown button (artefact + category). */ @@ -1469,11 +1522,11 @@ function block_exaport_category_list_item($category, $courseid, $type, $currentc /** * Different templates of artefact list. Depends on exaport settings */ -function block_exaport_artefact_list_item($item, $courseid, $type, $categoryid, $currentcategory) { +function block_exaport_artefact_list_item($item, $courseid, $type, $categoryid, $currentcategory, $foldermode = false) { $template = block_exaport_used_layout(); switch ($template) { case 'moodle_bootstrap': - return block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $categoryid, $currentcategory); + return block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $categoryid, $currentcategory, $foldermode); break; case 'exaport_bootstrap': // may we do not need this at all? return '
TODO: !!!!!! ' . $template . ' !!!!!!!
'; @@ -1488,95 +1541,59 @@ function block_exaport_artefact_list_item($item, $courseid, $type, $categoryid, function block_exaport_category_template_bootstrap_card($category, $courseid, $type, $currentcategory, $parentcategory = null) { global $CFG; - $categoryContent = ''; - // When showing the "go up" tile: mark it as fixed (not draggable) and use - // the parent category's ID so that dropping onto it moves items there. - $tileFixedClass = $parentcategory ? 'excomdos_tile_fixed ' : ''; - $tileTargetId = $parentcategory ? $parentcategory->id : $category->id; - // Resolve the display name now so we can add data-item-name to the wrapper. - $tileName = $parentcategory ? $parentcategory->name : $category->name; - $pinnedAttr = $parentcategory ? ' data-pinned="true"' : ''; - $categoryContent .= ' -
-
-
- - '; - if ($parentcategory) { - $categoryContent .= block_exaport_get_string('category_up'); - } elseif ($currentcategory->id == -1) { - $categoryContent .= block_exaport_get_string('user'); - } else { - $categoryContent .= block_exaport_get_string('category'); - } - $categoryContent .= ''; - // edit buttons - if (!$parentcategory) { - if ($type == 'shared' || $type == 'sharedstudent') { - $categoryContent .= block_exaport_fontawesome_icon('handshake', 'regular', 1); - } else { - // Type == mine. - if (@$category->internshare && (count(exaport_get_category_shared_users($category->id)) > 0 || - count(exaport_get_category_shared_groups($category->id)) > 0 || - (isset($category->shareall) && $category->shareall == 1))) { - $categoryContent .= block_exaport_fontawesome_icon('handshake', 'regular', 1); - }; - /*if (@$category->structure_share) { - $categoryContent .= ' '; - };*/ - $categoryContent .= ' - - ' - . block_exaport_fontawesome_icon('pen-to-square', 'regular', 1) - . ' - ' - . block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']) - . ' - '; + $isparenttile = (bool)$parentcategory; + $tiletargetid = $isparenttile ? (int)$parentcategory->id : (int)$category->id; + $tilename = $isparenttile ? $parentcategory->name : $category->name; + $tileurl = $isparenttile ? $parentcategory->url : $category->url; + $outerclasses = $isparenttile ? 'col mb-4 exaport-folder-category' : 'col col-card-folder mb-4 exaport-folder-category'; + $tilefixedclass = $isparenttile ? 'excomdos_tile_fixed ' : ''; + + $content = '
'; + $content .= '
'; + + if (!$isparenttile) { + $content .= ''; } - if ($parentcategory) { - $categoryThumbUrl = $parentcategory->url; - $categoryName = $parentcategory->name; - $categoryIcon = block_exaport_fontawesome_icon('folder-open', 'regular', '6', [], [], [], 'up', [], [], [], ['exaport-items-category-big']); + + $content .= ' - - -
-
- '; + $content .= format_string($tilename) . ''; + $content .= '
'; + $content .= ''; + $content .= '
'; - return $categoryContent; + return $content; } ; -function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $categoryid, $currentcategory) { +function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $categoryid, $currentcategory, $foldermode = false) { global $CFG, $USER, $DB; $iconTypeProps = block_exaport_item_icon_type_options($item->type); @@ -1590,6 +1607,70 @@ function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, } } + if ($foldermode) { + $itemtitle = format_string($item->name); + $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); + } + + $itemContent = '
'; + $itemContent .= '
'; + $itemContent .= '
'; + $itemContent .= '
' + . '' + . $itemtitle . '
'; + $itemContent .= ''; + $itemContent .= ''; + $itemContent .= '
'; + + $itemContent .= '
'; + if ($introtext !== '') { + $itemContent .= '

' . s($introtext) . '

'; + } + $itemContent .= '
'; + + $itemContent .= ''; + $itemContent .= '
'; + + return $itemContent; + } + $itemContent = '
From 9e568bd0c6f7dc39248bc31e301f78d378eddb54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:53:21 +0000 Subject: [PATCH 03/27] Finalize card redesign validation updates --- view_items.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/view_items.php b/view_items.php index 54c24615..2d183b63 100644 --- a/view_items.php +++ b/view_items.php @@ -1591,8 +1591,6 @@ function block_exaport_category_template_bootstrap_card($category, $courseid, $t return $content; } -; - function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $categoryid, $currentcategory, $foldermode = false) { global $CFG, $USER, $DB; From 13d2dc4c50788e78175037375505b78bca84f7e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:05:56 +0000 Subject: [PATCH 04/27] refactor: replace PHP string-concat card rendering with Mustache templates in view_items.php --- .../view_items_artefact_card_flat.mustache | 83 ++++++ .../view_items_artefact_card_folder.mustache | 82 ++++++ templates/view_items_category_card.mustache | 61 ++++ view_items.php | 274 +++++++----------- 4 files changed, 329 insertions(+), 171 deletions(-) create mode 100644 templates/view_items_artefact_card_flat.mustache create mode 100644 templates/view_items_artefact_card_folder.mustache create mode 100644 templates/view_items_category_card.mustache diff --git a/templates/view_items_artefact_card_flat.mustache b/templates/view_items_artefact_card_flat.mustache new file mode 100644 index 00000000..a576d71c --- /dev/null +++ b/templates/view_items_artefact_card_flat.mustache @@ -0,0 +1,83 @@ +{{! + Artefact card in flat/grid mode for the bootstrap layout (moodle_bootstrap). + Used by block_exaport_artefact_template_bootstrap_card() in view_items.php + when $foldermode is false. + + Context variables: + itemnamelower - Lowercase item name for data-item-name attribute + catids - Comma-separated category IDs for data-category-ids attribute + timemodified - Unix timestamp for data-item-date attribute + itemid - Integer item id + url - URL to the artefact detail page + itemname - Display name of the item (auto-escaped by mustache) + typeicon - Pre-rendered HTML for the type icon + typestring - Localised type label string + copytoself - Boolean: tile is in the "copy to portfolio" state (currentcategory.id == -1) + copytoselfurl - URL for the "make it yours" copy action (copytoself only) + copytoselftooltip - Localised tooltip for the copy-to-self button + hascomments - Boolean: item has at least one comment + commentcount - Integer comment count + commenticon - Pre-rendered HTML for the comment icon + projecticon - Pre-rendered HTML for the project info icon (may be empty) + compicon - Pre-rendered HTML for the competence icon (may be empty) + typemineorshared - Boolean: type is "mine" or "shared" (show edit/delete/user icons) + isownitem - Boolean: current user owns this item + editurl - URL for the edit action + editicon - Pre-rendered HTML for the edit icon + deleteurl - URL for the delete action + trashicon - Pre-rendered HTML for the delete icon + ownername - Full name of the item owner (shown in tooltip when not own item) + usericon - Pre-rendered HTML for the user icon + thumburl - URL to the item thumbnail image + categorybadges - Pre-rendered HTML for category badge pills (may be empty) + dateformatted - Human-readable date string for the card footer +}} +
+
+
+
+ + {{{typeicon}}}{{typestring}} + +
+
+ {{#copytoself}} + + + + {{/copytoself}} + {{^copytoself}} + {{#hascomments}} + {{commentcount}}{{{commenticon}}} + {{/hascomments}} + {{{projecticon}}} + {{{compicon}}} + {{#typemineorshared}} + {{#isownitem}} + {{{editicon}}} + {{{trashicon}}} + {{/isownitem}} + {{^isownitem}} + {{{usericon}}} + {{/isownitem}} + {{/typemineorshared}} + {{/copytoself}} +
+
+
+ + {{itemname}} + +
+
+ {{itemname}} + {{{categorybadges}}} +
+ +
+
diff --git a/templates/view_items_artefact_card_folder.mustache b/templates/view_items_artefact_card_folder.mustache new file mode 100644 index 00000000..335d9fec --- /dev/null +++ b/templates/view_items_artefact_card_folder.mustache @@ -0,0 +1,82 @@ +{{! + Artefact card in folder-navigation mode for the bootstrap layout (moodle_bootstrap). + Used by block_exaport_artefact_template_bootstrap_card() in view_items.php + when $foldermode is true. + + Context variables: + url - URL to the artefact detail page + itemnamelower - Lowercase item name for data-item-name attribute + timemodified - Unix timestamp for data-item-date attribute + catids - Comma-separated category IDs for data-category-ids attribute + itemid - Integer item id + typeicon - Pre-rendered HTML for the type icon (with tooltip) + itemname - Display name of the item (auto-escaped by mustache) + ellipsisicon - Pre-rendered HTML for the "⋮" dropdown toggle icon + viewlabel - Localised "View" label + viewicon - Pre-rendered HTML for the view icon + canedit - Boolean: show "Edit" menu entry + editurl - URL for the edit action + editicon - Pre-rendered HTML for the edit icon + editlabel - Localised "Edit" label + candelete - Boolean: show "Delete" menu entry + deleteurl - URL for the delete action + deleteicon - Pre-rendered HTML for the delete icon + deletelabel - Localised "Delete" label + introtext - Short plain-text intro excerpt (empty string when absent) + dateformatted - Human-readable date string for the card footer + compbadge - Pre-rendered HTML for the competence badge (may be empty) + hascomments - Boolean: item has at least one comment + commentcount - Integer comment count + commentlabel - Localised comment count tooltip string +}} +
+
+
+
+ {{{typeicon}}}{{itemname}} +
+ + + + +
+
+ {{#introtext}}

{{introtext}}

{{/introtext}} +
+ +
+
diff --git a/templates/view_items_category_card.mustache b/templates/view_items_category_card.mustache new file mode 100644 index 00000000..0cea8494 --- /dev/null +++ b/templates/view_items_category_card.mustache @@ -0,0 +1,61 @@ +{{! + Category card tile for the bootstrap layout (moodle_bootstrap). + Used by block_exaport_category_template_bootstrap_card() in view_items.php. + + Context variables: + outerclasses - CSS class string for the outer wrapper div + tilenamelower - Lowercase category name for data-item-name attribute + isparenttile - Boolean: true when this tile links *up* to the parent category + tiletargetid - Integer id of the target category (for drag-drop) + tilefixedclass - Extra CSS class string (non-empty only for parent tile) + tileurl - URL the tile links to + tilename - Display name of the category (auto-escaped by mustache) + typemine - Boolean: current user owns this category (show edit/delete) + editurl - URL for the edit action + deleteurl - URL for the delete action + ellipsisicon - Pre-rendered HTML for the "⋮" dropdown toggle icon + viewicon - Pre-rendered HTML for the "view" menu item icon + editicon - Pre-rendered HTML for the "edit" menu item icon + deleteicon - Pre-rendered HTML for the "delete" menu item icon + viewlabel - Localised "View" label + editlabel - Localised "Edit" label + deletelabel - Localised "Delete" label + folderupicon - Pre-rendered HTML for the folder-open-up icon (parent tile only) + categorylabel - Localised "Category" tooltip text (regular tile only) +}} + diff --git a/view_items.php b/view_items.php index 2d183b63..a2f653a0 100644 --- a/view_items.php +++ b/view_items.php @@ -1540,59 +1540,44 @@ function block_exaport_artefact_list_item($item, $courseid, $type, $categoryid, } function block_exaport_category_template_bootstrap_card($category, $courseid, $type, $currentcategory, $parentcategory = null) { - global $CFG; + global $CFG, $OUTPUT; $isparenttile = (bool)$parentcategory; $tiletargetid = $isparenttile ? (int)$parentcategory->id : (int)$category->id; $tilename = $isparenttile ? $parentcategory->name : $category->name; - $tileurl = $isparenttile ? $parentcategory->url : $category->url; + $tileurl = $isparenttile ? (string)$parentcategory->url : (string)$category->url; $outerclasses = $isparenttile ? 'col mb-4 exaport-folder-category' : 'col col-card-folder mb-4 exaport-folder-category'; $tilefixedclass = $isparenttile ? 'excomdos_tile_fixed ' : ''; - $content = ''; + $context = [ + 'outerclasses' => $outerclasses, + 'tilenamelower' => strtolower($tilename), + 'isparenttile' => $isparenttile, + 'tiletargetid' => $tiletargetid, + 'tilefixedclass' => $tilefixedclass, + 'tileurl' => $tileurl, + 'tilename' => $tilename, + 'typemine' => ($type == 'mine'), + 'editurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=edit', + 'deleteurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=delete', + 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'regular', 1), + 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), + 'editicon' => block_exaport_fontawesome_icon('pen-to-square', 'regular', 1), + 'deleteicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), + 'viewlabel' => block_exaport_get_string('view'), + 'editlabel' => block_exaport_get_string('edit'), + 'deletelabel' => block_exaport_get_string('delete'), + 'folderupicon' => block_exaport_fontawesome_icon('folder-open', 'regular', 1, ['icon', 'fa-fw', 'me-1'], [], + ['data-bs-toggle' => 'tooltip', 'data-bs-placement' => 'top', + 'data-bs-title' => block_exaport_get_string('category_up')], 'up'), + 'categorylabel' => block_exaport_get_string('category'), + ]; - return $content; + return $OUTPUT->render_from_template('block_exaport/view_items_category_card', $context); } function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $categoryid, $currentcategory, $foldermode = false) { - global $CFG, $USER, $DB; + global $CFG, $USER, $DB, $OUTPUT; $iconTypeProps = block_exaport_item_icon_type_options($item->type); $url = $CFG->wwwroot . '/blocks/exaport/shared_item.php?courseid=' . $courseid . '&access=portfolio/id/' . $item->userid . '&itemid=' . $item->id; @@ -1606,7 +1591,6 @@ function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, } if ($foldermode) { - $itemtitle = format_string($item->name); $typelabel = get_string($item->type, 'block_exaport'); $introtext = ''; if (!empty($item->intro)) { @@ -1615,133 +1599,81 @@ function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $introtext = shorten_text(trim(strip_tags($intro)), 140, true); } - $itemContent = '
'; - $itemContent .= '
'; - $itemContent .= '
'; - $itemContent .= '
' - . '' - . $itemtitle . '
'; - $itemContent .= ''; - $itemContent .= ''; - $itemContent .= '
'; - - $itemContent .= '
'; - if ($introtext !== '') { - $itemContent .= '

' . s($introtext) . '

'; - } - $itemContent .= '
'; - - $itemContent .= ''; - $itemContent .= '
'; - - return $itemContent; - } - - $itemContent = ' -
-
-
-
- ' - . block_exaport_fontawesome_icon($iconTypeProps['iconName'], $iconTypeProps['iconStyle'], 1, ['artefact_icon']) - . '' . get_string($item->type, "block_exaport") . ' -
-
'; - - if ($currentcategory->id == -1) { - // Link to export to portfolio. - $itemContent .= ' '; - } else { - if ($item->comments > 0) { - $itemContent .= ' ' . $item->comments - . block_exaport_fontawesome_icon('comment', 'regular', 1, [], [], [], '', [], [], [], []) - . ''; - } - $itemContent .= block_exaport_get_item_project_icon($item); - $itemContent .= block_exaport_get_item_comp_icon($item); - - if (in_array($type, ['mine', 'shared'])) { - $cattype = ''; - if ($type == 'shared') { - $cattype = '&cattype=shared'; - } - if ($item->userid == $USER->id) { // only for self! - $itemContent .= '' - . block_exaport_fontawesome_icon('pen-to-square', 'regular', 1) - . ''; - } - if (($type == 'mine' && $allowedit = block_exaport_item_is_editable($item->id)) // strange condition. If exacomp is not used - always allowed! - || $item->userid == $USER->id) { - if ($item->userid == $USER->id) { - $itemContent .= '' - . block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']) - . ''; - } - } else if (!$allowedit = block_exaport_item_is_editable($item->id)) { - $itemContent .= 'file'; - } - if ($item->userid != $USER->id) { - $itemuser = $DB->get_record('user', ['id' => $item->userid]); - // user icon - $itemContent .= '' - . block_exaport_fontawesome_icon('circle-user', 'solid', 1) - . ''; - } - } - } - - $itemContent .= '
-
-
- - ' . $item->name . ' - -
-
- ' . $item->name . ' - ' . block_exaport_render_item_category_badges($item) . ' -
- -
-
- '; + $cattype = ($type == 'shared') ? '&cattype=shared' : ''; + $isownitem = ($item->userid == $USER->id); + $commentcount = (int)($item->comments ?? 0); + $commentlabel = $commentcount . ' ' . block_exaport_get_string($commentcount === 1 ? 'comment' : 'comments'); + + $context = [ + 'url' => $url, + 'itemnamelower' => strtolower($item->name), + 'timemodified' => (int)$item->timemodified, + 'catids' => implode(',', $itemCatIds), + 'itemid' => (int)$item->id, + 'typeicon' => '', + 'itemname' => $item->name, + 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'regular', 1), + 'viewlabel' => block_exaport_get_string('view'), + 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), + 'canedit' => $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), + 'editlabel' => block_exaport_get_string('edit'), + 'candelete' => $isownitem && block_exaport_item_is_editable($item->id), + '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']), + 'deletelabel' => block_exaport_get_string('delete'), + 'introtext' => $introtext, + 'dateformatted' => date('d.m.Y H:i', $item->timemodified), + 'compbadge' => block_exaport_get_item_comp_footer_badge($item), + 'hascomments' => $commentcount > 0, + 'commentcount' => $commentcount, + 'commentlabel' => $commentlabel, + ]; + + return $OUTPUT->render_from_template('block_exaport/view_items_artefact_card_folder', $context); + } + + // Flat / grid mode. + $copytoself = ($currentcategory->id == -1); + $isownitem = ($item->userid == $USER->id); + $ownername = ''; + if (!$isownitem) { + $itemuser = $DB->get_record('user', ['id' => $item->userid]); + $ownername = $itemuser ? fullname($itemuser) : ''; + } + + $context = [ + 'itemnamelower' => strtolower($item->name), + 'catids' => implode(',', $itemCatIds), + 'timemodified' => (int)$item->timemodified, + 'itemid' => (int)$item->id, + 'url' => $url, + 'itemname' => $item->name, + 'typeicon' => block_exaport_fontawesome_icon($iconTypeProps['iconName'], $iconTypeProps['iconStyle'], 1, ['artefact_icon']), + 'typestring' => get_string($item->type, 'block_exaport'), + 'copytoself' => $copytoself, + 'copytoselfurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid . '&id=' . $item->id . '&sesskey=' . sesskey() . '&action=copytoself', + 'copytoselftooltip' => get_string('make_it_yours', 'block_exaport'), + 'hascomments' => ($item->comments > 0), + 'commentcount' => (int)$item->comments, + 'commenticon' => block_exaport_fontawesome_icon('comment', 'regular', 1, [], [], [], '', [], [], [], []), + 'projecticon' => block_exaport_get_item_project_icon($item), + 'compicon' => block_exaport_get_item_comp_icon($item), + 'typemineorshared' => in_array($type, ['mine', 'shared']), + 'isownitem' => $isownitem, + 'editurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid . '&id=' . $item->id . '&action=edit' . (($type == 'shared') ? '&cattype=shared' : ''), + '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 . (($type == 'shared') ? '&cattype=shared' : ''), + 'trashicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), + 'ownername' => $ownername, + 'usericon' => block_exaport_fontawesome_icon('circle-user', 'solid', 1), + 'thumburl' => $CFG->wwwroot . '/blocks/exaport/item_thumb.php?item_id=' . $item->id, + 'categorybadges' => block_exaport_render_item_category_badges($item), + 'dateformatted' => date('d.m.Y H:i', $item->timemodified), + ]; - return $itemContent; + return $OUTPUT->render_from_template('block_exaport/view_items_artefact_card_flat', $context); } From f31b42112d9991046ca137053b31c80d439d8fd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:18:02 +0000 Subject: [PATCH 05/27] Fix FA free icons and inline fixed icons in mustache templates --- templates/view_items_artefact_card_flat.mustache | 12 ++++-------- templates/view_items_artefact_card_folder.mustache | 12 ++++-------- templates/view_items_category_card.mustache | 12 ++++-------- view_items.php | 12 ------------ 4 files changed, 12 insertions(+), 36 deletions(-) diff --git a/templates/view_items_artefact_card_flat.mustache b/templates/view_items_artefact_card_flat.mustache index a576d71c..3a80d353 100644 --- a/templates/view_items_artefact_card_flat.mustache +++ b/templates/view_items_artefact_card_flat.mustache @@ -17,17 +17,13 @@ copytoselftooltip - Localised tooltip for the copy-to-self button hascomments - Boolean: item has at least one comment commentcount - Integer comment count - commenticon - Pre-rendered HTML for the comment icon projecticon - Pre-rendered HTML for the project info icon (may be empty) compicon - Pre-rendered HTML for the competence icon (may be empty) typemineorshared - Boolean: type is "mine" or "shared" (show edit/delete/user icons) isownitem - Boolean: current user owns this item editurl - URL for the edit action - editicon - Pre-rendered HTML for the edit icon deleteurl - URL for the delete action - trashicon - Pre-rendered HTML for the delete icon ownername - Full name of the item owner (shown in tooltip when not own item) - usericon - Pre-rendered HTML for the user icon thumburl - URL to the item thumbnail image categorybadges - Pre-rendered HTML for category badge pills (may be empty) dateformatted - Human-readable date string for the card footer @@ -51,17 +47,17 @@ {{/copytoself}} {{^copytoself}} {{#hascomments}} - {{commentcount}}{{{commenticon}}} + {{commentcount}} {{/hascomments}} {{{projecticon}}} {{{compicon}}} {{#typemineorshared}} {{#isownitem}} - {{{editicon}}} - {{{trashicon}}} + + {{/isownitem}} {{^isownitem}} - {{{usericon}}} + {{/isownitem}} {{/typemineorshared}} {{/copytoself}} diff --git a/templates/view_items_artefact_card_folder.mustache b/templates/view_items_artefact_card_folder.mustache index 335d9fec..35ef3c89 100644 --- a/templates/view_items_artefact_card_folder.mustache +++ b/templates/view_items_artefact_card_folder.mustache @@ -11,16 +11,12 @@ itemid - Integer item id typeicon - Pre-rendered HTML for the type icon (with tooltip) itemname - Display name of the item (auto-escaped by mustache) - ellipsisicon - Pre-rendered HTML for the "⋮" dropdown toggle icon viewlabel - Localised "View" label - viewicon - Pre-rendered HTML for the view icon canedit - Boolean: show "Edit" menu entry editurl - URL for the edit action - editicon - Pre-rendered HTML for the edit icon editlabel - Localised "Edit" label candelete - Boolean: show "Delete" menu entry deleteurl - URL for the delete action - deleteicon - Pre-rendered HTML for the delete icon deletelabel - Localised "Delete" label introtext - Short plain-text intro excerpt (empty string when absent) dateformatted - Human-readable date string for the card footer @@ -40,20 +36,20 @@ diff --git a/templates/view_items_category_card.mustache b/templates/view_items_category_card.mustache index 0cea8494..71c49c3e 100644 --- a/templates/view_items_category_card.mustache +++ b/templates/view_items_category_card.mustache @@ -13,10 +13,6 @@ typemine - Boolean: current user owns this category (show edit/delete) editurl - URL for the edit action deleteurl - URL for the delete action - ellipsisicon - Pre-rendered HTML for the "⋮" dropdown toggle icon - viewicon - Pre-rendered HTML for the "view" menu item icon - editicon - Pre-rendered HTML for the "edit" menu item icon - deleteicon - Pre-rendered HTML for the "delete" menu item icon viewlabel - Localised "View" label editlabel - Localised "Edit" label deletelabel - Localised "Delete" label @@ -29,18 +25,18 @@
diff --git a/view_items.php b/view_items.php index a2f653a0..c11e7fc2 100644 --- a/view_items.php +++ b/view_items.php @@ -1560,10 +1560,6 @@ function block_exaport_category_template_bootstrap_card($category, $courseid, $t 'typemine' => ($type == 'mine'), 'editurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=edit', 'deleteurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=delete', - 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'regular', 1), - 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), - 'editicon' => block_exaport_fontawesome_icon('pen-to-square', 'regular', 1), - 'deleteicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), 'viewlabel' => block_exaport_get_string('view'), 'editlabel' => block_exaport_get_string('edit'), 'deletelabel' => block_exaport_get_string('delete'), @@ -1614,16 +1610,12 @@ function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, . ' data-bs-toggle="tooltip" data-bs-placement="top"' . ' data-bs-title="' . s($typelabel) . '">', 'itemname' => $item->name, - 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'regular', 1), 'viewlabel' => block_exaport_get_string('view'), - 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), 'canedit' => $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), 'editlabel' => block_exaport_get_string('edit'), 'candelete' => $isownitem && block_exaport_item_is_editable($item->id), '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']), 'deletelabel' => block_exaport_get_string('delete'), 'introtext' => $introtext, 'dateformatted' => date('d.m.Y H:i', $item->timemodified), @@ -1659,17 +1651,13 @@ function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, 'copytoselftooltip' => get_string('make_it_yours', 'block_exaport'), 'hascomments' => ($item->comments > 0), 'commentcount' => (int)$item->comments, - 'commenticon' => block_exaport_fontawesome_icon('comment', 'regular', 1, [], [], [], '', [], [], [], []), 'projecticon' => block_exaport_get_item_project_icon($item), 'compicon' => block_exaport_get_item_comp_icon($item), 'typemineorshared' => in_array($type, ['mine', 'shared']), 'isownitem' => $isownitem, 'editurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid . '&id=' . $item->id . '&action=edit' . (($type == 'shared') ? '&cattype=shared' : ''), - '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 . (($type == 'shared') ? '&cattype=shared' : ''), - 'trashicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), 'ownername' => $ownername, - 'usericon' => block_exaport_fontawesome_icon('circle-user', 'solid', 1), 'thumburl' => $CFG->wwwroot . '/blocks/exaport/item_thumb.php?item_id=' . $item->id, 'categorybadges' => block_exaport_render_item_category_badges($item), 'dateformatted' => date('d.m.Y H:i', $item->timemodified), From 32cb4f043fcd6dd6f017c15ce95832db553fc2e6 Mon Sep 17 00:00:00 2001 From: rwolf Date: Tue, 2 Jun 2026 12:25:53 +0200 Subject: [PATCH 06/27] Revert "Fix FA free icons and inline fixed icons in mustache templates" This reverts commit f31b42112d9991046ca137053b31c80d439d8fd2. --- templates/view_items_artefact_card_flat.mustache | 12 ++++++++---- templates/view_items_artefact_card_folder.mustache | 12 ++++++++---- templates/view_items_category_card.mustache | 12 ++++++++---- view_items.php | 12 ++++++++++++ 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/templates/view_items_artefact_card_flat.mustache b/templates/view_items_artefact_card_flat.mustache index 3a80d353..a576d71c 100644 --- a/templates/view_items_artefact_card_flat.mustache +++ b/templates/view_items_artefact_card_flat.mustache @@ -17,13 +17,17 @@ copytoselftooltip - Localised tooltip for the copy-to-self button hascomments - Boolean: item has at least one comment commentcount - Integer comment count + commenticon - Pre-rendered HTML for the comment icon projecticon - Pre-rendered HTML for the project info icon (may be empty) compicon - Pre-rendered HTML for the competence icon (may be empty) typemineorshared - Boolean: type is "mine" or "shared" (show edit/delete/user icons) isownitem - Boolean: current user owns this item editurl - URL for the edit action + editicon - Pre-rendered HTML for the edit icon deleteurl - URL for the delete action + trashicon - Pre-rendered HTML for the delete icon ownername - Full name of the item owner (shown in tooltip when not own item) + usericon - Pre-rendered HTML for the user icon thumburl - URL to the item thumbnail image categorybadges - Pre-rendered HTML for category badge pills (may be empty) dateformatted - Human-readable date string for the card footer @@ -47,17 +51,17 @@ {{/copytoself}} {{^copytoself}} {{#hascomments}} - {{commentcount}} + {{commentcount}}{{{commenticon}}} {{/hascomments}} {{{projecticon}}} {{{compicon}}} {{#typemineorshared}} {{#isownitem}} - - + {{{editicon}}} + {{{trashicon}}} {{/isownitem}} {{^isownitem}} - + {{{usericon}}} {{/isownitem}} {{/typemineorshared}} {{/copytoself}} diff --git a/templates/view_items_artefact_card_folder.mustache b/templates/view_items_artefact_card_folder.mustache index 35ef3c89..335d9fec 100644 --- a/templates/view_items_artefact_card_folder.mustache +++ b/templates/view_items_artefact_card_folder.mustache @@ -11,12 +11,16 @@ itemid - Integer item id typeicon - Pre-rendered HTML for the type icon (with tooltip) itemname - Display name of the item (auto-escaped by mustache) + ellipsisicon - Pre-rendered HTML for the "⋮" dropdown toggle icon viewlabel - Localised "View" label + viewicon - Pre-rendered HTML for the view icon canedit - Boolean: show "Edit" menu entry editurl - URL for the edit action + editicon - Pre-rendered HTML for the edit icon editlabel - Localised "Edit" label candelete - Boolean: show "Delete" menu entry deleteurl - URL for the delete action + deleteicon - Pre-rendered HTML for the delete icon deletelabel - Localised "Delete" label introtext - Short plain-text intro excerpt (empty string when absent) dateformatted - Human-readable date string for the card footer @@ -36,20 +40,20 @@ diff --git a/templates/view_items_category_card.mustache b/templates/view_items_category_card.mustache index 71c49c3e..0cea8494 100644 --- a/templates/view_items_category_card.mustache +++ b/templates/view_items_category_card.mustache @@ -13,6 +13,10 @@ typemine - Boolean: current user owns this category (show edit/delete) editurl - URL for the edit action deleteurl - URL for the delete action + ellipsisicon - Pre-rendered HTML for the "⋮" dropdown toggle icon + viewicon - Pre-rendered HTML for the "view" menu item icon + editicon - Pre-rendered HTML for the "edit" menu item icon + deleteicon - Pre-rendered HTML for the "delete" menu item icon viewlabel - Localised "View" label editlabel - Localised "Edit" label deletelabel - Localised "Delete" label @@ -25,18 +29,18 @@
diff --git a/view_items.php b/view_items.php index c11e7fc2..a2f653a0 100644 --- a/view_items.php +++ b/view_items.php @@ -1560,6 +1560,10 @@ function block_exaport_category_template_bootstrap_card($category, $courseid, $t 'typemine' => ($type == 'mine'), 'editurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=edit', 'deleteurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=delete', + 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'regular', 1), + 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), + 'editicon' => block_exaport_fontawesome_icon('pen-to-square', 'regular', 1), + 'deleteicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), 'viewlabel' => block_exaport_get_string('view'), 'editlabel' => block_exaport_get_string('edit'), 'deletelabel' => block_exaport_get_string('delete'), @@ -1610,12 +1614,16 @@ function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, . ' data-bs-toggle="tooltip" data-bs-placement="top"' . ' data-bs-title="' . s($typelabel) . '">', 'itemname' => $item->name, + 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'regular', 1), 'viewlabel' => block_exaport_get_string('view'), + 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), 'canedit' => $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), 'editlabel' => block_exaport_get_string('edit'), 'candelete' => $isownitem && block_exaport_item_is_editable($item->id), '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']), 'deletelabel' => block_exaport_get_string('delete'), 'introtext' => $introtext, 'dateformatted' => date('d.m.Y H:i', $item->timemodified), @@ -1651,13 +1659,17 @@ function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, 'copytoselftooltip' => get_string('make_it_yours', 'block_exaport'), 'hascomments' => ($item->comments > 0), 'commentcount' => (int)$item->comments, + 'commenticon' => block_exaport_fontawesome_icon('comment', 'regular', 1, [], [], [], '', [], [], [], []), 'projecticon' => block_exaport_get_item_project_icon($item), 'compicon' => block_exaport_get_item_comp_icon($item), 'typemineorshared' => in_array($type, ['mine', 'shared']), 'isownitem' => $isownitem, 'editurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid . '&id=' . $item->id . '&action=edit' . (($type == 'shared') ? '&cattype=shared' : ''), + '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 . (($type == 'shared') ? '&cattype=shared' : ''), + 'trashicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), 'ownername' => $ownername, + 'usericon' => block_exaport_fontawesome_icon('circle-user', 'solid', 1), 'thumburl' => $CFG->wwwroot . '/blocks/exaport/item_thumb.php?item_id=' . $item->id, 'categorybadges' => block_exaport_render_item_category_badges($item), 'dateformatted' => date('d.m.Y H:i', $item->timemodified), From 6a8f2273ca4d82d4a6eab7fe1b8f9909163f7f90 Mon Sep 17 00:00:00 2001 From: rwolf Date: Tue, 2 Jun 2026 12:27:08 +0200 Subject: [PATCH 07/27] fix pro to free fontawesome --- view_items.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/view_items.php b/view_items.php index a2f653a0..21a3e739 100644 --- a/view_items.php +++ b/view_items.php @@ -1560,7 +1560,7 @@ function block_exaport_category_template_bootstrap_card($category, $courseid, $t 'typemine' => ($type == 'mine'), 'editurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=edit', 'deleteurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=delete', - 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'regular', 1), + 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'solid', 1), 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), 'editicon' => block_exaport_fontawesome_icon('pen-to-square', 'regular', 1), 'deleteicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), @@ -1614,7 +1614,7 @@ function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, . ' data-bs-toggle="tooltip" data-bs-placement="top"' . ' data-bs-title="' . s($typelabel) . '">', 'itemname' => $item->name, - 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'regular', 1), + 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'solid', 1), 'viewlabel' => block_exaport_get_string('view'), 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), 'canedit' => $isownitem, From 942c5b5224a583429f636754479f4cdd0d44ffb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:31:50 +0000 Subject: [PATCH 08/27] Fix: make category card-footer consistent height with item card-footer --- templates/view_items_category_card.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/view_items_category_card.mustache b/templates/view_items_category_card.mustache index 0cea8494..59755857 100644 --- a/templates/view_items_category_card.mustache +++ b/templates/view_items_category_card.mustache @@ -56,6 +56,6 @@
- +
From c129a4f917993205cb81eaab1b0c03d8488bc3e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:43:50 +0000 Subject: [PATCH 09/27] Refactor: extract output classes and move utility functions to lib.php --- classes/output/artefact_card_flat.php | 144 +++++++++++++ classes/output/artefact_card_folder.php | 144 +++++++++++++ classes/output/category_card.php | 113 ++++++++++ lib/lib.php | 121 +++++++++++ renderer.php | 40 ++++ view_items.php | 275 ++---------------------- 6 files changed, 576 insertions(+), 261 deletions(-) create mode 100644 classes/output/artefact_card_flat.php create mode 100644 classes/output/artefact_card_folder.php create mode 100644 classes/output/category_card.php diff --git a/classes/output/artefact_card_flat.php b/classes/output/artefact_card_flat.php new file mode 100644 index 00000000..572bab64 --- /dev/null +++ b/classes/output/artefact_card_flat.php @@ -0,0 +1,144 @@ +. +// (c) 2016 GTN - Global Training Network GmbH . + +namespace block_exaport\output; + +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; + +/** + * Output class for the artefact card in flat/grid mode (Bootstrap layout). + * + * Renders block_exaport/view_items_artefact_card_flat. + */ +class artefact_card_flat 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 (id == -1 means "copy to self" mode). + */ + 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; + } + + /** + * Export the data required by the mustache template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + global $CFG, $USER, $DB; + + $item = $this->item; + $courseid = $this->courseid; + $type = $this->type; + $categoryid = $this->categoryid; + $currentcategory = $this->currentcategory; + + $iconTypeProps = block_exaport_item_icon_type_options($item->type); + $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; + } + } + + $copytoself = ($currentcategory->id == -1); + $isownitem = ($item->userid == $USER->id); + $ownername = ''; + if (!$isownitem) { + $itemuser = $DB->get_record('user', ['id' => $item->userid]); + $ownername = $itemuser ? fullname($itemuser) : ''; + } + + return [ + 'itemnamelower' => strtolower($item->name), + 'catids' => implode(',', $itemCatIds), + 'timemodified' => (int)$item->timemodified, + 'itemid' => (int)$item->id, + 'url' => $url, + 'itemname' => $item->name, + 'typeicon' => block_exaport_fontawesome_icon($iconTypeProps['iconName'], $iconTypeProps['iconStyle'], 1, ['artefact_icon']), + 'typestring' => get_string($item->type, 'block_exaport'), + 'copytoself' => $copytoself, + 'copytoselfurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid + . '&id=' . $item->id . '&sesskey=' . sesskey() . '&action=copytoself', + 'copytoselftooltip' => get_string('make_it_yours', 'block_exaport'), + 'hascomments' => ($item->comments > 0), + 'commentcount' => (int)$item->comments, + 'commenticon' => block_exaport_fontawesome_icon('comment', 'regular', 1, [], [], [], '', [], [], [], []), + 'projecticon' => block_exaport_get_item_project_icon($item), + 'compicon' => block_exaport_get_item_comp_icon($item), + 'typemineorshared' => in_array($type, ['mine', 'shared']), + 'isownitem' => $isownitem, + 'editurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid + . '&id=' . $item->id . '&action=edit' . (($type == 'shared') ? '&cattype=shared' : ''), + '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 + . (($type == 'shared') ? '&cattype=shared' : ''), + 'trashicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), + 'ownername' => $ownername, + 'usericon' => block_exaport_fontawesome_icon('circle-user', 'solid', 1), + 'thumburl' => $CFG->wwwroot . '/blocks/exaport/item_thumb.php?item_id=' . $item->id, + 'categorybadges' => block_exaport_render_item_category_badges($item), + 'dateformatted' => date('d.m.Y H:i', $item->timemodified), + ]; + } + + /** + * Return the mustache template name. + * + * @return string + */ + public function get_template_name(): string { + return 'block_exaport/view_items_artefact_card_flat'; + } +} diff --git a/classes/output/artefact_card_folder.php b/classes/output/artefact_card_folder.php new file mode 100644 index 00000000..c8c486f7 --- /dev/null +++ b/classes/output/artefact_card_folder.php @@ -0,0 +1,144 @@ +. +// (c) 2016 GTN - Global Training Network GmbH . + +namespace block_exaport\output; + +defined('MOODLE_INTERNAL') || die(); + +use context_user; +use renderable; +use renderer_base; +use templatable; + +/** + * Output class for the artefact card in folder-navigation mode (Bootstrap layout). + * + * Renders block_exaport/view_items_artefact_card_folder. + */ +class artefact_card_folder implements renderable, templatable { + + /** @var \stdClass $item */ + protected $item; + + /** @var int $courseid */ + protected $courseid; + + /** @var string $type */ + protected $type; + + /** @var int $categoryid */ + protected $categoryid; + + /** + * 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 (kept for API parity). + */ + 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; + } + + /** + * Export the data required by the mustache template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + global $CFG, $USER; + + $item = $this->item; + $courseid = $this->courseid; + $type = $this->type; + $categoryid = $this->categoryid; + + $iconTypeProps = block_exaport_item_icon_type_options($item->type); + $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; + } + } + + $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); + } + + $cattype = ($type == 'shared') ? '&cattype=shared' : ''; + $isownitem = ($item->userid == $USER->id); + $commentcount = (int)($item->comments ?? 0); + $commentlabel = $commentcount . ' ' . block_exaport_get_string($commentcount === 1 ? 'comment' : 'comments'); + + return [ + 'url' => $url, + 'itemnamelower' => strtolower($item->name), + 'timemodified' => (int)$item->timemodified, + 'catids' => implode(',', $itemCatIds), + 'itemid' => (int)$item->id, + 'typeicon' => '', + 'itemname' => $item->name, + 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'solid', 1), + 'viewlabel' => block_exaport_get_string('view'), + 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), + 'canedit' => $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), + 'editlabel' => block_exaport_get_string('edit'), + 'candelete' => $isownitem && block_exaport_item_is_editable($item->id), + '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']), + 'deletelabel' => block_exaport_get_string('delete'), + 'introtext' => $introtext, + 'dateformatted' => date('d.m.Y H:i', $item->timemodified), + 'compbadge' => block_exaport_get_item_comp_footer_badge($item), + 'hascomments' => $commentcount > 0, + 'commentcount' => $commentcount, + 'commentlabel' => $commentlabel, + ]; + } + + /** + * Return the mustache template name. + * + * @return string + */ + public function get_template_name(): string { + return 'block_exaport/view_items_artefact_card_folder'; + } +} diff --git a/classes/output/category_card.php b/classes/output/category_card.php new file mode 100644 index 00000000..a14d2002 --- /dev/null +++ b/classes/output/category_card.php @@ -0,0 +1,113 @@ +. +// (c) 2016 GTN - Global Training Network GmbH . + +namespace block_exaport\output; + +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; + +/** + * Output class for the category card tile (Bootstrap/folder-mode layout). + * + * Renders block_exaport/view_items_category_card. + */ +class category_card implements renderable, templatable { + + /** @var \stdClass $category */ + protected $category; + + /** @var int $courseid */ + protected $courseid; + + /** @var string $type */ + protected $type; + + /** @var \stdClass|null $parentcategory */ + protected $parentcategory; + + /** + * Constructor. + * + * @param \stdClass $category The category record. + * @param int $courseid The course id. + * @param string $type Access type, e.g. 'mine' or 'shared'. + * @param \stdClass $currentcategory The currently active category (unused in context build but kept for API parity). + * @param \stdClass|null $parentcategory When non-null, this tile links up to the parent category. + */ + public function __construct(\stdClass $category, int $courseid, string $type, \stdClass $currentcategory, + ?\stdClass $parentcategory = null) { + $this->category = $category; + $this->courseid = $courseid; + $this->type = $type; + $this->parentcategory = $parentcategory; + } + + /** + * Export the data required by the mustache template. + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + global $CFG; + + $isparenttile = (bool)$this->parentcategory; + $tiletargetid = $isparenttile ? (int)$this->parentcategory->id : (int)$this->category->id; + $tilename = $isparenttile ? $this->parentcategory->name : $this->category->name; + $tileurl = $isparenttile ? (string)$this->parentcategory->url : (string)$this->category->url; + $outerclasses = $isparenttile ? 'col mb-4 exaport-folder-category' : 'col col-card-folder mb-4 exaport-folder-category'; + $tilefixedclass = $isparenttile ? 'excomdos_tile_fixed ' : ''; + + return [ + 'outerclasses' => $outerclasses, + 'tilenamelower' => strtolower($tilename), + 'isparenttile' => $isparenttile, + 'tiletargetid' => $tiletargetid, + 'tilefixedclass' => $tilefixedclass, + 'tileurl' => $tileurl, + 'tilename' => $tilename, + 'typemine' => ($this->type == 'mine'), + 'editurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $this->courseid + . '&id=' . $this->category->id . '&action=edit', + 'deleteurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $this->courseid + . '&id=' . $this->category->id . '&action=delete', + 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'solid', 1), + 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), + 'editicon' => block_exaport_fontawesome_icon('pen-to-square', 'regular', 1), + 'deleteicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), + 'viewlabel' => block_exaport_get_string('view'), + 'editlabel' => block_exaport_get_string('edit'), + 'deletelabel' => block_exaport_get_string('delete'), + 'folderupicon' => block_exaport_fontawesome_icon('folder-open', 'regular', 1, ['icon', 'fa-fw', 'me-1'], [], + ['data-bs-toggle' => 'tooltip', 'data-bs-placement' => 'top', + 'data-bs-title' => block_exaport_get_string('category_up')], 'up'), + 'categorylabel' => block_exaport_get_string('category'), + ]; + } + + /** + * Return the mustache template name. + * + * @return string + */ + public function get_template_name(): string { + return 'block_exaport/view_items_category_card'; + } +} diff --git a/lib/lib.php b/lib/lib.php index 9e4391fc..03d4c811 100644 --- a/lib/lib.php +++ b/lib/lib.php @@ -3105,3 +3105,124 @@ function block_exaport_get_comment_author_name($userid) { // Fallback if user not found return get_string('unknownuser', 'block_exaport'); } + +/** + * Build a category tree keyed by parent id. + * + * @param array $categories + * @return array + */ +function block_exaport_build_categories_by_parent(array $categories) { + $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. + */ +function block_exaport_category_full_path_name($categoryid, array $categories) { + $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. + */ +function block_exaport_load_flat_items($userid, array $categories, $sqlsort, $allowedcategoryids = null) { + 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 = block_exaport_category_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; +} diff --git a/renderer.php b/renderer.php index 63c04c33..69d7c590 100644 --- a/renderer.php +++ b/renderer.php @@ -19,6 +19,46 @@ require_once(__DIR__ . '/inc.php'); class block_exaport_renderer extends plugin_renderer_base { + + /** + * Render a category card tile (Bootstrap/folder-mode layout). + * + * @param \block_exaport\output\category_card $card + * @return string HTML + */ + public function render_category_card(\block_exaport\output\category_card $card): string { + return $this->render_from_template( + 'block_exaport/view_items_category_card', + $card->export_for_template($this) + ); + } + + /** + * Render an artefact card in folder-navigation mode (Bootstrap layout). + * + * @param \block_exaport\output\artefact_card_folder $card + * @return string HTML + */ + public function render_artefact_card_folder(\block_exaport\output\artefact_card_folder $card): string { + return $this->render_from_template( + 'block_exaport/view_items_artefact_card_folder', + $card->export_for_template($this) + ); + } + + /** + * Render an artefact card in flat/grid mode (Bootstrap layout). + * + * @param \block_exaport\output\artefact_card_flat $card + * @return string HTML + */ + public function render_artefact_card_flat(\block_exaport\output\artefact_card_flat $card): string { + return $this->render_from_template( + 'block_exaport/view_items_artefact_card_flat', + $card->export_for_template($this) + ); + } + /** * in moodle33 pix_url was renamed to image_url */ diff --git a/view_items.php b/view_items.php index 21a3e739..34e4be78 100644 --- a/view_items.php +++ b/view_items.php @@ -1157,127 +1157,6 @@ function block_exaport_render_item_category_badges($item) { return html_writer::div(implode('', $badges), 'mt-2'); } -/** - * Build a category tree keyed by parent id. - * - * @param array $categories - * @return array - */ -function block_exaport_build_categories_by_parent(array $categories) { - $categoriesbyparent = []; - foreach ($categories as $category) { - if (!isset($categoriesbyparent[$category->pid])) { - $categoriesbyparent[$category->pid] = []; - } - $categoriesbyparent[$category->pid][] = $category; - } - - return $categoriesbyparent; -} - -/** - * 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. - */ -function block_exaport_load_flat_items($userid, array $categories, $sqlsort, $allowedcategoryids = null) { - 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 = block_exaport_category_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; -} - -/** - * 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. - */ -function block_exaport_category_full_path_name($categoryid, array $categories) { - $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); -} - function block_exaport_category_path($category, $courseid = 1, $currentcategoryPathItemButtons = '') { global $DB, $CFG; $pathItem = function($id, $title, $courseid, $selected = false, $currentcategoryPathItemButtons = '') use ($CFG) { @@ -1503,10 +1382,13 @@ function block_exaport_artefact_template_tile($item, $courseid, $type, $category * Different templates of category list. Depends on exaport settings */ function block_exaport_category_list_item($category, $courseid, $type, $currentcategory, $parentcategory = null) { + global $PAGE; $template = block_exaport_used_layout(); switch ($template) { case 'moodle_bootstrap': - return block_exaport_category_template_bootstrap_card($category, $courseid, $type, $currentcategory, $parentcategory); + return $PAGE->get_renderer('block_exaport')->render( + new \block_exaport\output\category_card($category, $courseid, $type, $currentcategory, $parentcategory) + ); break; case 'exaport_bootstrap': // may we do not need this at all? return '
TODO: !!!!!! ' . $template . ' category !!!!!!!
'; @@ -1523,10 +1405,19 @@ function block_exaport_category_list_item($category, $courseid, $type, $currentc * Different templates of artefact list. Depends on exaport settings */ function block_exaport_artefact_list_item($item, $courseid, $type, $categoryid, $currentcategory, $foldermode = false) { + global $PAGE; $template = block_exaport_used_layout(); switch ($template) { case 'moodle_bootstrap': - return block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $categoryid, $currentcategory, $foldermode); + if ($foldermode) { + return $PAGE->get_renderer('block_exaport')->render( + new \block_exaport\output\artefact_card_folder($item, $courseid, $type, $categoryid, $currentcategory) + ); + } else { + return $PAGE->get_renderer('block_exaport')->render( + new \block_exaport\output\artefact_card_flat($item, $courseid, $type, $categoryid, $currentcategory) + ); + } break; case 'exaport_bootstrap': // may we do not need this at all? return '
TODO: !!!!!! ' . $template . ' !!!!!!!
'; @@ -1539,141 +1430,3 @@ function block_exaport_artefact_list_item($item, $courseid, $type, $categoryid, } -function block_exaport_category_template_bootstrap_card($category, $courseid, $type, $currentcategory, $parentcategory = null) { - global $CFG, $OUTPUT; - - $isparenttile = (bool)$parentcategory; - $tiletargetid = $isparenttile ? (int)$parentcategory->id : (int)$category->id; - $tilename = $isparenttile ? $parentcategory->name : $category->name; - $tileurl = $isparenttile ? (string)$parentcategory->url : (string)$category->url; - $outerclasses = $isparenttile ? 'col mb-4 exaport-folder-category' : 'col col-card-folder mb-4 exaport-folder-category'; - $tilefixedclass = $isparenttile ? 'excomdos_tile_fixed ' : ''; - - $context = [ - 'outerclasses' => $outerclasses, - 'tilenamelower' => strtolower($tilename), - 'isparenttile' => $isparenttile, - 'tiletargetid' => $tiletargetid, - 'tilefixedclass' => $tilefixedclass, - 'tileurl' => $tileurl, - 'tilename' => $tilename, - 'typemine' => ($type == 'mine'), - 'editurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=edit', - 'deleteurl' => $CFG->wwwroot . '/blocks/exaport/category.php?courseid=' . $courseid . '&id=' . $category->id . '&action=delete', - 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'solid', 1), - 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), - 'editicon' => block_exaport_fontawesome_icon('pen-to-square', 'regular', 1), - 'deleteicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), - 'viewlabel' => block_exaport_get_string('view'), - 'editlabel' => block_exaport_get_string('edit'), - 'deletelabel' => block_exaport_get_string('delete'), - 'folderupicon' => block_exaport_fontawesome_icon('folder-open', 'regular', 1, ['icon', 'fa-fw', 'me-1'], [], - ['data-bs-toggle' => 'tooltip', 'data-bs-placement' => 'top', - 'data-bs-title' => block_exaport_get_string('category_up')], 'up'), - 'categorylabel' => block_exaport_get_string('category'), - ]; - - return $OUTPUT->render_from_template('block_exaport/view_items_category_card', $context); -} - -function block_exaport_artefact_template_bootstrap_card($item, $courseid, $type, $categoryid, $currentcategory, $foldermode = false) { - global $CFG, $USER, $DB, $OUTPUT; - - $iconTypeProps = block_exaport_item_icon_type_options($item->type); - $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; - } - } - - if ($foldermode) { - $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); - } - - $cattype = ($type == 'shared') ? '&cattype=shared' : ''; - $isownitem = ($item->userid == $USER->id); - $commentcount = (int)($item->comments ?? 0); - $commentlabel = $commentcount . ' ' . block_exaport_get_string($commentcount === 1 ? 'comment' : 'comments'); - - $context = [ - 'url' => $url, - 'itemnamelower' => strtolower($item->name), - 'timemodified' => (int)$item->timemodified, - 'catids' => implode(',', $itemCatIds), - 'itemid' => (int)$item->id, - 'typeicon' => '', - 'itemname' => $item->name, - 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'solid', 1), - 'viewlabel' => block_exaport_get_string('view'), - 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), - 'canedit' => $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), - 'editlabel' => block_exaport_get_string('edit'), - 'candelete' => $isownitem && block_exaport_item_is_editable($item->id), - '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']), - 'deletelabel' => block_exaport_get_string('delete'), - 'introtext' => $introtext, - 'dateformatted' => date('d.m.Y H:i', $item->timemodified), - 'compbadge' => block_exaport_get_item_comp_footer_badge($item), - 'hascomments' => $commentcount > 0, - 'commentcount' => $commentcount, - 'commentlabel' => $commentlabel, - ]; - - return $OUTPUT->render_from_template('block_exaport/view_items_artefact_card_folder', $context); - } - - // Flat / grid mode. - $copytoself = ($currentcategory->id == -1); - $isownitem = ($item->userid == $USER->id); - $ownername = ''; - if (!$isownitem) { - $itemuser = $DB->get_record('user', ['id' => $item->userid]); - $ownername = $itemuser ? fullname($itemuser) : ''; - } - - $context = [ - 'itemnamelower' => strtolower($item->name), - 'catids' => implode(',', $itemCatIds), - 'timemodified' => (int)$item->timemodified, - 'itemid' => (int)$item->id, - 'url' => $url, - 'itemname' => $item->name, - 'typeicon' => block_exaport_fontawesome_icon($iconTypeProps['iconName'], $iconTypeProps['iconStyle'], 1, ['artefact_icon']), - 'typestring' => get_string($item->type, 'block_exaport'), - 'copytoself' => $copytoself, - 'copytoselfurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid . '&id=' . $item->id . '&sesskey=' . sesskey() . '&action=copytoself', - 'copytoselftooltip' => get_string('make_it_yours', 'block_exaport'), - 'hascomments' => ($item->comments > 0), - 'commentcount' => (int)$item->comments, - 'commenticon' => block_exaport_fontawesome_icon('comment', 'regular', 1, [], [], [], '', [], [], [], []), - 'projecticon' => block_exaport_get_item_project_icon($item), - 'compicon' => block_exaport_get_item_comp_icon($item), - 'typemineorshared' => in_array($type, ['mine', 'shared']), - 'isownitem' => $isownitem, - 'editurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid . '&id=' . $item->id . '&action=edit' . (($type == 'shared') ? '&cattype=shared' : ''), - '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 . (($type == 'shared') ? '&cattype=shared' : ''), - 'trashicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), - 'ownername' => $ownername, - 'usericon' => block_exaport_fontawesome_icon('circle-user', 'solid', 1), - 'thumburl' => $CFG->wwwroot . '/blocks/exaport/item_thumb.php?item_id=' . $item->id, - 'categorybadges' => block_exaport_render_item_category_badges($item), - 'dateformatted' => date('d.m.Y H:i', $item->timemodified), - ]; - - return $OUTPUT->render_from_template('block_exaport/view_items_artefact_card_flat', $context); -} From b565efec8db823bb6baee1fa70b600a83354fd30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:56:38 +0000 Subject: [PATCH 10/27] Remove redundant render_* methods from renderer.php --- renderer.php | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/renderer.php b/renderer.php index 69d7c590..613ac93f 100644 --- a/renderer.php +++ b/renderer.php @@ -20,45 +20,6 @@ class block_exaport_renderer extends plugin_renderer_base { - /** - * Render a category card tile (Bootstrap/folder-mode layout). - * - * @param \block_exaport\output\category_card $card - * @return string HTML - */ - public function render_category_card(\block_exaport\output\category_card $card): string { - return $this->render_from_template( - 'block_exaport/view_items_category_card', - $card->export_for_template($this) - ); - } - - /** - * Render an artefact card in folder-navigation mode (Bootstrap layout). - * - * @param \block_exaport\output\artefact_card_folder $card - * @return string HTML - */ - public function render_artefact_card_folder(\block_exaport\output\artefact_card_folder $card): string { - return $this->render_from_template( - 'block_exaport/view_items_artefact_card_folder', - $card->export_for_template($this) - ); - } - - /** - * Render an artefact card in flat/grid mode (Bootstrap layout). - * - * @param \block_exaport\output\artefact_card_flat $card - * @return string HTML - */ - public function render_artefact_card_flat(\block_exaport\output\artefact_card_flat $card): string { - return $this->render_from_template( - 'block_exaport/view_items_artefact_card_flat', - $card->export_for_template($this) - ); - } - /** * in moodle33 pix_url was renamed to image_url */ From 83269ce046ea1773311e611c70f4c505eab8dcb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:00:37 +0000 Subject: [PATCH 11/27] Move category helper functions to classes/category_helper.php --- classes/category_helper.php | 145 ++++++++++++++++++++++++++++++++++++ lib/lib.php | 121 ------------------------------ view_items.php | 16 ++-- 3 files changed, 153 insertions(+), 129 deletions(-) create mode 100644 classes/category_helper.php diff --git a/classes/category_helper.php b/classes/category_helper.php new file mode 100644 index 00000000..2c327b9d --- /dev/null +++ b/classes/category_helper.php @@ -0,0 +1,145 @@ +. +// (c) 2016 GTN - Global Training Network GmbH . + +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. + $isviewingotheruser = $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 ($isviewingotheruser) { + $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; + } +} diff --git a/lib/lib.php b/lib/lib.php index 03d4c811..9e4391fc 100644 --- a/lib/lib.php +++ b/lib/lib.php @@ -3105,124 +3105,3 @@ function block_exaport_get_comment_author_name($userid) { // Fallback if user not found return get_string('unknownuser', 'block_exaport'); } - -/** - * Build a category tree keyed by parent id. - * - * @param array $categories - * @return array - */ -function block_exaport_build_categories_by_parent(array $categories) { - $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. - */ -function block_exaport_category_full_path_name($categoryid, array $categories) { - $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. - */ -function block_exaport_load_flat_items($userid, array $categories, $sqlsort, $allowedcategoryids = null) { - 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 = block_exaport_category_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; -} diff --git a/view_items.php b/view_items.php index 34e4be78..d35cfc88 100644 --- a/view_items.php +++ b/view_items.php @@ -164,7 +164,7 @@ } // Build a tree according to parent. - $categoriesbyparent = block_exaport_build_categories_by_parent($categories); + $categoriesbyparent = \block_exaport\category_helper::build_by_parent($categories); // The main root category for student. $rootcategory = block_exaport_get_root_category($selecteduser->id); @@ -197,7 +197,7 @@ $currentcategory = $rootcategory; $parentcategory = null; $subcategories = []; - $items = block_exaport_load_flat_items($selecteduser->id, $categories, $sqlsort); + $items = \block_exaport\category_helper::load_flat_items($selecteduser->id, $categories, $sqlsort); } else { // Common items. $items = $DB->get_records_sql(" @@ -295,7 +295,7 @@ function category_allowed($selecteduser, $categories, $category) { $category->icon = block_exaport_get_category_icon($category); } // Build a tree according to parent. - $categoriesbyparent = block_exaport_build_categories_by_parent($categories); + $categoriesbyparent = \block_exaport\category_helper::build_by_parent($categories); if (!isset($categories[$categoryid])) { throw new moodle_exception('not allowed'); @@ -334,7 +334,7 @@ function category_allowed($selecteduser, $categories, $category) { $currentcategory = $rootcategory; $parentcategory = null; $subcategories = []; - $items = block_exaport_load_flat_items($selecteduser->id, $categories, $sqlsort, array_keys($allowedcategories)); + $items = \block_exaport\category_helper::load_flat_items($selecteduser->id, $categories, $sqlsort, array_keys($allowedcategories)); } else { $usercondition = ' i.userid = ' . intval($selecteduser->id) . ' '; if ($type == 'shared') { @@ -373,7 +373,7 @@ function category_allowed($selecteduser, $categories, $category) { } // Build a tree according to parent. - $categoriesbyparent = block_exaport_build_categories_by_parent($categories); + $categoriesbyparent = \block_exaport\category_helper::build_by_parent($categories); // The main root category. $rootcategory = block_exaport_get_root_category(); @@ -407,9 +407,9 @@ function category_allowed($selecteduser, $categories, $category) { // Filter out the root category (id=0) which is a virtual placeholder, not a real DB category. $usercategoryids = array_filter(array_keys($categories), fn($id) => $id > 0); if ($show_otherusers) { - $items = block_exaport_load_flat_items($USER->id, $categories, $sqlsort, $usercategoryids ?: null); + $items = \block_exaport\category_helper::load_flat_items($USER->id, $categories, $sqlsort, $usercategoryids ?: null); } else { - $items = block_exaport_load_flat_items($USER->id, $categories, $sqlsort, null); + $items = \block_exaport\category_helper::load_flat_items($USER->id, $categories, $sqlsort, null); } } else { // Folder mode keeps legacy category navigation behavior. @@ -523,7 +523,7 @@ function block_exaport_print_category_select($categoriesbyparent, $currentcatego if ($type == 'shared' && !category_allowed($selecteduser, $categories, $category)) { continue; } - $filtercategories[(int)$category->id] = block_exaport_category_full_path_name($category->id, $categories); + $filtercategories[(int)$category->id] = \block_exaport\category_helper::full_path_name($category->id, $categories); } echo '
'; From 4bb969c728953455e3a250b52fec375fe438264c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:09:07 +0000 Subject: [PATCH 12/27] Reduce duplication in artefact card output classes via shared base class --- classes/output/artefact_card.php | 114 ++++++++++++++++++ classes/output/artefact_card_flat.php | 79 ++---------- classes/output/artefact_card_folder.php | 101 +++------------- .../view_items_artefact_card_flat.mustache | 4 +- 4 files changed, 142 insertions(+), 156 deletions(-) create mode 100644 classes/output/artefact_card.php diff --git a/classes/output/artefact_card.php b/classes/output/artefact_card.php new file mode 100644 index 00000000..1a9bab54 --- /dev/null +++ b/classes/output/artefact_card.php @@ -0,0 +1,114 @@ +. +// (c) 2016 GTN - Global Training Network GmbH . + +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), + ]; + } +} diff --git a/classes/output/artefact_card_flat.php b/classes/output/artefact_card_flat.php index 572bab64..c9d885e2 100644 --- a/classes/output/artefact_card_flat.php +++ b/classes/output/artefact_card_flat.php @@ -19,49 +19,14 @@ defined('MOODLE_INTERNAL') || die(); -use renderable; use renderer_base; -use templatable; /** * Output class for the artefact card in flat/grid mode (Bootstrap layout). * * Renders block_exaport/view_items_artefact_card_flat. */ -class artefact_card_flat 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 (id == -1 means "copy to self" mode). - */ - 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; - } +class artefact_card_flat extends artefact_card { /** * Export the data required by the mustache template. @@ -70,66 +35,36 @@ public function __construct(\stdClass $item, int $courseid, string $type, int $c * @return array */ public function export_for_template(renderer_base $output): array { - global $CFG, $USER, $DB; + global $CFG, $DB, $USER; $item = $this->item; $courseid = $this->courseid; - $type = $this->type; - $categoryid = $this->categoryid; $currentcategory = $this->currentcategory; $iconTypeProps = block_exaport_item_icon_type_options($item->type); - $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; - } - } - - $copytoself = ($currentcategory->id == -1); - $isownitem = ($item->userid == $USER->id); - $ownername = ''; + $copytoself = ($currentcategory->id == -1); + $isownitem = ($item->userid == $USER->id); + $ownername = ''; if (!$isownitem) { $itemuser = $DB->get_record('user', ['id' => $item->userid]); $ownername = $itemuser ? fullname($itemuser) : ''; } - return [ - 'itemnamelower' => strtolower($item->name), - 'catids' => implode(',', $itemCatIds), - 'timemodified' => (int)$item->timemodified, - 'itemid' => (int)$item->id, - 'url' => $url, - 'itemname' => $item->name, + return $this->base_export_data() + [ 'typeicon' => block_exaport_fontawesome_icon($iconTypeProps['iconName'], $iconTypeProps['iconStyle'], 1, ['artefact_icon']), 'typestring' => get_string($item->type, 'block_exaport'), 'copytoself' => $copytoself, 'copytoselfurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid . '&id=' . $item->id . '&sesskey=' . sesskey() . '&action=copytoself', 'copytoselftooltip' => get_string('make_it_yours', 'block_exaport'), - 'hascomments' => ($item->comments > 0), - 'commentcount' => (int)$item->comments, 'commenticon' => block_exaport_fontawesome_icon('comment', 'regular', 1, [], [], [], '', [], [], [], []), 'projecticon' => block_exaport_get_item_project_icon($item), 'compicon' => block_exaport_get_item_comp_icon($item), - 'typemineorshared' => in_array($type, ['mine', 'shared']), - 'isownitem' => $isownitem, - 'editurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid - . '&id=' . $item->id . '&action=edit' . (($type == 'shared') ? '&cattype=shared' : ''), - '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 - . (($type == 'shared') ? '&cattype=shared' : ''), - 'trashicon' => block_exaport_fontawesome_icon('trash-can', 'regular', 1, [], [], [], '', [], [], [], ['exaport-remove-icon']), + 'typemineorshared' => in_array($this->type, ['mine', 'shared']), 'ownername' => $ownername, 'usericon' => block_exaport_fontawesome_icon('circle-user', 'solid', 1), 'thumburl' => $CFG->wwwroot . '/blocks/exaport/item_thumb.php?item_id=' . $item->id, 'categorybadges' => block_exaport_render_item_category_badges($item), - 'dateformatted' => date('d.m.Y H:i', $item->timemodified), ]; } diff --git a/classes/output/artefact_card_folder.php b/classes/output/artefact_card_folder.php index c8c486f7..7940a22d 100644 --- a/classes/output/artefact_card_folder.php +++ b/classes/output/artefact_card_folder.php @@ -20,45 +20,14 @@ defined('MOODLE_INTERNAL') || die(); use context_user; -use renderable; use renderer_base; -use templatable; /** * Output class for the artefact card in folder-navigation mode (Bootstrap layout). * * Renders block_exaport/view_items_artefact_card_folder. */ -class artefact_card_folder implements renderable, templatable { - - /** @var \stdClass $item */ - protected $item; - - /** @var int $courseid */ - protected $courseid; - - /** @var string $type */ - protected $type; - - /** @var int $categoryid */ - protected $categoryid; - - /** - * 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 (kept for API parity). - */ - 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; - } +class artefact_card_folder extends artefact_card { /** * Export the data required by the mustache template. @@ -67,26 +36,10 @@ public function __construct(\stdClass $item, int $courseid, string $type, int $c * @return array */ public function export_for_template(renderer_base $output): array { - global $CFG, $USER; - - $item = $this->item; - $courseid = $this->courseid; - $type = $this->type; - $categoryid = $this->categoryid; - + $item = $this->item; $iconTypeProps = block_exaport_item_icon_type_options($item->type); - $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; - } - } + $typelabel = get_string($item->type, 'block_exaport'); - $typelabel = get_string($item->type, 'block_exaport'); $introtext = ''; if (!empty($item->intro)) { $intro = file_rewrite_pluginfile_urls($item->intro, 'pluginfile.php', @@ -96,40 +49,24 @@ public function export_for_template(renderer_base $output): array { $introtext = shorten_text(trim(strip_tags($intro)), 140, true); } - $cattype = ($type == 'shared') ? '&cattype=shared' : ''; - $isownitem = ($item->userid == $USER->id); - $commentcount = (int)($item->comments ?? 0); + $base = $this->base_export_data(); + $commentcount = $base['commentcount']; $commentlabel = $commentcount . ' ' . block_exaport_get_string($commentcount === 1 ? 'comment' : 'comments'); - return [ - 'url' => $url, - 'itemnamelower' => strtolower($item->name), - 'timemodified' => (int)$item->timemodified, - 'catids' => implode(',', $itemCatIds), - 'itemid' => (int)$item->id, - 'typeicon' => '', - 'itemname' => $item->name, - 'ellipsisicon' => block_exaport_fontawesome_icon('ellipsis-vertical', 'solid', 1), - 'viewlabel' => block_exaport_get_string('view'), - 'viewicon' => block_exaport_fontawesome_icon('eye', 'regular', 1), - 'canedit' => $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), - 'editlabel' => block_exaport_get_string('edit'), - 'candelete' => $isownitem && block_exaport_item_is_editable($item->id), - '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']), - 'deletelabel' => block_exaport_get_string('delete'), - 'introtext' => $introtext, - 'dateformatted' => date('d.m.Y H:i', $item->timemodified), - 'compbadge' => block_exaport_get_item_comp_footer_badge($item), - 'hascomments' => $commentcount > 0, - 'commentcount' => $commentcount, - 'commentlabel' => $commentlabel, + return $base + [ + 'typeicon' => '', + '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, ]; } diff --git a/templates/view_items_artefact_card_flat.mustache b/templates/view_items_artefact_card_flat.mustache index a576d71c..d1d61644 100644 --- a/templates/view_items_artefact_card_flat.mustache +++ b/templates/view_items_artefact_card_flat.mustache @@ -25,7 +25,7 @@ editurl - URL for the edit action editicon - Pre-rendered HTML for the edit icon deleteurl - URL for the delete action - trashicon - Pre-rendered HTML for the delete icon + deleteicon - Pre-rendered HTML for the delete icon ownername - Full name of the item owner (shown in tooltip when not own item) usericon - Pre-rendered HTML for the user icon thumburl - URL to the item thumbnail image @@ -58,7 +58,7 @@ {{#typemineorshared}} {{#isownitem}} {{{editicon}}} - {{{trashicon}}} + {{{deleteicon}}} {{/isownitem}} {{^isownitem}} {{{usericon}}} From 7f498a117d056da523bd69c1d1b17f6c5ffbda48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:09:58 +0000 Subject: [PATCH 13/27] Fix variable naming style in category_helper.php --- classes/category_helper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/category_helper.php b/classes/category_helper.php index 2c327b9d..632fd889 100644 --- a/classes/category_helper.php +++ b/classes/category_helper.php @@ -100,7 +100,7 @@ public static function load_flat_items(int $userid, array $categories, string $s // Belt-and-suspenders: restrict to the viewed user's own categories, // even though items are already scoped by userid. - $isviewingotheruser = $allowedcategoryids === null && (int)$userid !== (int)$USER->id; + $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 @@ -110,7 +110,7 @@ public static function load_flat_items(int $userid, array $categories, string $s // Belt-and-suspenders: restrict to the viewed user's own categories, // even though items are already scoped by userid. - if ($isviewingotheruser) { + if ($is_viewing_other_user) { $sql .= " AND c.userid = ?"; $params[] = $userid; } From da8f1721acc629c8a8acf4666000594a3df06b59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:07:00 +0000 Subject: [PATCH 14/27] rename templates to match output class names --- classes/output/artefact_card_flat.php | 4 ++-- classes/output/artefact_card_folder.php | 4 ++-- classes/output/category_card.php | 4 ++-- ...rtefact_card_flat.mustache => artefact_card_flat.mustache} | 0 ...act_card_folder.mustache => artefact_card_folder.mustache} | 0 ...ew_items_category_card.mustache => category_card.mustache} | 0 6 files changed, 6 insertions(+), 6 deletions(-) rename templates/{view_items_artefact_card_flat.mustache => artefact_card_flat.mustache} (100%) rename templates/{view_items_artefact_card_folder.mustache => artefact_card_folder.mustache} (100%) rename templates/{view_items_category_card.mustache => category_card.mustache} (100%) diff --git a/classes/output/artefact_card_flat.php b/classes/output/artefact_card_flat.php index c9d885e2..ce63a233 100644 --- a/classes/output/artefact_card_flat.php +++ b/classes/output/artefact_card_flat.php @@ -24,7 +24,7 @@ /** * Output class for the artefact card in flat/grid mode (Bootstrap layout). * - * Renders block_exaport/view_items_artefact_card_flat. + * Renders block_exaport/artefact_card_flat. */ class artefact_card_flat extends artefact_card { @@ -74,6 +74,6 @@ public function export_for_template(renderer_base $output): array { * @return string */ public function get_template_name(): string { - return 'block_exaport/view_items_artefact_card_flat'; + return 'block_exaport/artefact_card_flat'; } } diff --git a/classes/output/artefact_card_folder.php b/classes/output/artefact_card_folder.php index 7940a22d..2027c852 100644 --- a/classes/output/artefact_card_folder.php +++ b/classes/output/artefact_card_folder.php @@ -25,7 +25,7 @@ /** * Output class for the artefact card in folder-navigation mode (Bootstrap layout). * - * Renders block_exaport/view_items_artefact_card_folder. + * Renders block_exaport/artefact_card_folder. */ class artefact_card_folder extends artefact_card { @@ -76,6 +76,6 @@ public function export_for_template(renderer_base $output): array { * @return string */ public function get_template_name(): string { - return 'block_exaport/view_items_artefact_card_folder'; + return 'block_exaport/artefact_card_folder'; } } diff --git a/classes/output/category_card.php b/classes/output/category_card.php index a14d2002..7fa1a85b 100644 --- a/classes/output/category_card.php +++ b/classes/output/category_card.php @@ -26,7 +26,7 @@ /** * Output class for the category card tile (Bootstrap/folder-mode layout). * - * Renders block_exaport/view_items_category_card. + * Renders block_exaport/category_card. */ class category_card implements renderable, templatable { @@ -108,6 +108,6 @@ public function export_for_template(renderer_base $output): array { * @return string */ public function get_template_name(): string { - return 'block_exaport/view_items_category_card'; + return 'block_exaport/category_card'; } } diff --git a/templates/view_items_artefact_card_flat.mustache b/templates/artefact_card_flat.mustache similarity index 100% rename from templates/view_items_artefact_card_flat.mustache rename to templates/artefact_card_flat.mustache diff --git a/templates/view_items_artefact_card_folder.mustache b/templates/artefact_card_folder.mustache similarity index 100% rename from templates/view_items_artefact_card_folder.mustache rename to templates/artefact_card_folder.mustache diff --git a/templates/view_items_category_card.mustache b/templates/category_card.mustache similarity index 100% rename from templates/view_items_category_card.mustache rename to templates/category_card.mustache From 2c088e0d5aac88fc7a41ff8f2f48ee1a94d71b51 Mon Sep 17 00:00:00 2001 From: rwolf Date: Tue, 2 Jun 2026 14:10:29 +0200 Subject: [PATCH 15/27] cleanup --- classes/output/artefact_card_flat.php | 9 --------- classes/output/artefact_card_folder.php | 9 --------- classes/output/category_card.php | 9 --------- version.php | 2 +- 4 files changed, 1 insertion(+), 28 deletions(-) diff --git a/classes/output/artefact_card_flat.php b/classes/output/artefact_card_flat.php index ce63a233..a5f91a7c 100644 --- a/classes/output/artefact_card_flat.php +++ b/classes/output/artefact_card_flat.php @@ -67,13 +67,4 @@ public function export_for_template(renderer_base $output): array { 'categorybadges' => block_exaport_render_item_category_badges($item), ]; } - - /** - * Return the mustache template name. - * - * @return string - */ - public function get_template_name(): string { - return 'block_exaport/artefact_card_flat'; - } } diff --git a/classes/output/artefact_card_folder.php b/classes/output/artefact_card_folder.php index 2027c852..d6b39913 100644 --- a/classes/output/artefact_card_folder.php +++ b/classes/output/artefact_card_folder.php @@ -69,13 +69,4 @@ public function export_for_template(renderer_base $output): array { 'commentlabel' => $commentlabel, ]; } - - /** - * Return the mustache template name. - * - * @return string - */ - public function get_template_name(): string { - return 'block_exaport/artefact_card_folder'; - } } diff --git a/classes/output/category_card.php b/classes/output/category_card.php index 7fa1a85b..93a91232 100644 --- a/classes/output/category_card.php +++ b/classes/output/category_card.php @@ -101,13 +101,4 @@ public function export_for_template(renderer_base $output): array { 'categorylabel' => block_exaport_get_string('category'), ]; } - - /** - * Return the mustache template name. - * - * @return string - */ - public function get_template_name(): string { - return 'block_exaport/category_card'; - } } diff --git a/version.php b/version.php index 3ae1d1b6..cdbe8a2b 100644 --- a/version.php +++ b/version.php @@ -19,6 +19,6 @@ $plugin->component = 'block_exaport'; $plugin->release = '5.1'; -$plugin->version = 2026052903; +$plugin->version = 2026060202; $plugin->requires = 2021051700; // moodle 3.11 $plugin->maturity = MATURITY_STABLE; From 8e63c49c9239833d8ef3bc0bf581509d2a4c22cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:19:37 +0000 Subject: [PATCH 16/27] unify artefact card design for folder and flat views --- classes/output/artefact_card_flat.php | 37 ++--------- renderer.php | 12 ++++ templates/artefact_card_flat.mustache | 83 ------------------------- templates/artefact_card_folder.mustache | 8 ++- 4 files changed, 22 insertions(+), 118 deletions(-) delete mode 100644 templates/artefact_card_flat.mustache diff --git a/classes/output/artefact_card_flat.php b/classes/output/artefact_card_flat.php index a5f91a7c..1097e895 100644 --- a/classes/output/artefact_card_flat.php +++ b/classes/output/artefact_card_flat.php @@ -24,9 +24,10 @@ /** * Output class for the artefact card in flat/grid mode (Bootstrap layout). * - * Renders block_exaport/artefact_card_flat. + * 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 { +class artefact_card_flat extends artefact_card_folder { /** * Export the data required by the mustache template. @@ -35,36 +36,8 @@ class artefact_card_flat extends artefact_card { * @return array */ public function export_for_template(renderer_base $output): array { - global $CFG, $DB, $USER; - - $item = $this->item; - $courseid = $this->courseid; - $currentcategory = $this->currentcategory; - - $iconTypeProps = block_exaport_item_icon_type_options($item->type); - $copytoself = ($currentcategory->id == -1); - $isownitem = ($item->userid == $USER->id); - $ownername = ''; - if (!$isownitem) { - $itemuser = $DB->get_record('user', ['id' => $item->userid]); - $ownername = $itemuser ? fullname($itemuser) : ''; - } - - return $this->base_export_data() + [ - 'typeicon' => block_exaport_fontawesome_icon($iconTypeProps['iconName'], $iconTypeProps['iconStyle'], 1, ['artefact_icon']), - 'typestring' => get_string($item->type, 'block_exaport'), - 'copytoself' => $copytoself, - 'copytoselfurl' => $CFG->wwwroot . '/blocks/exaport/item.php?courseid=' . $courseid - . '&id=' . $item->id . '&sesskey=' . sesskey() . '&action=copytoself', - 'copytoselftooltip' => get_string('make_it_yours', 'block_exaport'), - 'commenticon' => block_exaport_fontawesome_icon('comment', 'regular', 1, [], [], [], '', [], [], [], []), - 'projecticon' => block_exaport_get_item_project_icon($item), - 'compicon' => block_exaport_get_item_comp_icon($item), - 'typemineorshared' => in_array($this->type, ['mine', 'shared']), - 'ownername' => $ownername, - 'usericon' => block_exaport_fontawesome_icon('circle-user', 'solid', 1), - 'thumburl' => $CFG->wwwroot . '/blocks/exaport/item_thumb.php?item_id=' . $item->id, - 'categorybadges' => block_exaport_render_item_category_badges($item), + return parent::export_for_template($output) + [ + 'categorybadges' => block_exaport_render_item_category_badges($this->item), ]; } } diff --git a/renderer.php b/renderer.php index 613ac93f..10dcb66b 100644 --- a/renderer.php +++ b/renderer.php @@ -41,5 +41,17 @@ public function get_theme_dir() { public function get_theme_config() { return $this->page->theme; } + + /** + * Render an artefact card in flat/grid mode using the shared folder card template. + * + * The flat card is identical to the folder card except it also shows category chips. + * + * @param \block_exaport\output\artefact_card_flat $card + * @return string + */ + public function render_artefact_card_flat(\block_exaport\output\artefact_card_flat $card): string { + return $this->render_from_template('block_exaport/artefact_card_folder', $card->export_for_template($this)); + } } diff --git a/templates/artefact_card_flat.mustache b/templates/artefact_card_flat.mustache deleted file mode 100644 index d1d61644..00000000 --- a/templates/artefact_card_flat.mustache +++ /dev/null @@ -1,83 +0,0 @@ -{{! - Artefact card in flat/grid mode for the bootstrap layout (moodle_bootstrap). - Used by block_exaport_artefact_template_bootstrap_card() in view_items.php - when $foldermode is false. - - Context variables: - itemnamelower - Lowercase item name for data-item-name attribute - catids - Comma-separated category IDs for data-category-ids attribute - timemodified - Unix timestamp for data-item-date attribute - itemid - Integer item id - url - URL to the artefact detail page - itemname - Display name of the item (auto-escaped by mustache) - typeicon - Pre-rendered HTML for the type icon - typestring - Localised type label string - copytoself - Boolean: tile is in the "copy to portfolio" state (currentcategory.id == -1) - copytoselfurl - URL for the "make it yours" copy action (copytoself only) - copytoselftooltip - Localised tooltip for the copy-to-self button - hascomments - Boolean: item has at least one comment - commentcount - Integer comment count - commenticon - Pre-rendered HTML for the comment icon - projecticon - Pre-rendered HTML for the project info icon (may be empty) - compicon - Pre-rendered HTML for the competence icon (may be empty) - typemineorshared - Boolean: type is "mine" or "shared" (show edit/delete/user icons) - isownitem - Boolean: current user owns this item - editurl - URL for the edit action - editicon - Pre-rendered HTML for the edit icon - deleteurl - URL for the delete action - deleteicon - Pre-rendered HTML for the delete icon - ownername - Full name of the item owner (shown in tooltip when not own item) - usericon - Pre-rendered HTML for the user icon - thumburl - URL to the item thumbnail image - categorybadges - Pre-rendered HTML for category badge pills (may be empty) - dateformatted - Human-readable date string for the card footer -}} -
-
-
-
- - {{{typeicon}}}{{typestring}} - -
-
- {{#copytoself}} - - - - {{/copytoself}} - {{^copytoself}} - {{#hascomments}} - {{commentcount}}{{{commenticon}}} - {{/hascomments}} - {{{projecticon}}} - {{{compicon}}} - {{#typemineorshared}} - {{#isownitem}} - {{{editicon}}} - {{{deleteicon}}} - {{/isownitem}} - {{^isownitem}} - {{{usericon}}} - {{/isownitem}} - {{/typemineorshared}} - {{/copytoself}} -
-
-
- - {{itemname}} - -
-
- {{itemname}} - {{{categorybadges}}} -
- -
-
diff --git a/templates/artefact_card_folder.mustache b/templates/artefact_card_folder.mustache index 335d9fec..ccabe989 100644 --- a/templates/artefact_card_folder.mustache +++ b/templates/artefact_card_folder.mustache @@ -1,7 +1,7 @@ {{! - Artefact card in folder-navigation mode for the bootstrap layout (moodle_bootstrap). + Artefact card for the bootstrap layout (moodle_bootstrap). Used by block_exaport_artefact_template_bootstrap_card() in view_items.php - when $foldermode is true. + for both folder-navigation mode and flat/grid mode. Context variables: url - URL to the artefact detail page @@ -28,8 +28,9 @@ hascomments - Boolean: item has at least one comment commentcount - Integer comment count commentlabel - Localised comment count tooltip string + categorybadges - Pre-rendered HTML for category badge pills (flat mode only, may be empty) }} -
@@ -61,6 +62,7 @@
{{#introtext}}

{{introtext}}

{{/introtext}} + {{#categorybadges}}
{{{categorybadges}}}
{{/categorybadges}}
- {{#introtext}}

{{introtext}}

{{/introtext}} - {{#categorybadges}}
{{{categorybadges}}}
{{/categorybadges}} +
+ {{#introtext}}

{{introtext}}

{{/introtext}} +
+
+ {{{categorybadges}}} +