MDL-14636: Support table view in Filepicker, provide more info about files
- Added table view in Filepicker where for each file we show Name, Last modified, Size and Type. Sortable by column - Preprocess list of files returned from repositories to include formatted size, dates, type and also add filetype icon - Make sure that local repositories (coursefiles, filesystem, local, recent, user) return information about file size, datemodified, datecreated, author, license
This commit is contained in:
@@ -179,6 +179,7 @@ $string['submit'] = 'Submit';
|
||||
$string['sync'] = 'Sync';
|
||||
$string['thumbview'] = 'View as icons';
|
||||
$string['title'] = 'Choose a file...';
|
||||
$string['type'] = 'Type';
|
||||
$string['typenotvisible'] = 'Type not visible';
|
||||
$string['upload'] = 'Upload this file';
|
||||
$string['uploading'] = 'Uploading...';
|
||||
|
||||
@@ -439,7 +439,7 @@ class page_requirements_manager {
|
||||
case 'core_filepicker':
|
||||
$module = array('name' => 'core_filepicker',
|
||||
'fullpath' => '/repository/filepicker.js',
|
||||
'requires' => array('base', 'node', 'node-event-simulate', 'json', 'async-queue', 'io-base', 'io-upload-iframe', 'io-form', 'yui2-menu', 'yui2-treeview', 'yui2-dragdrop', 'panel', 'cookie'),
|
||||
'requires' => array('base', 'node', 'node-event-simulate', 'json', 'async-queue', 'io-base', 'io-upload-iframe', 'io-form', 'yui2-menu', 'yui2-treeview', 'yui2-dragdrop', 'panel', 'cookie', 'datatable', 'datatable-sort'),
|
||||
// TODO check if those all are really required
|
||||
'strings' => array(array('add', 'repository'), array('back', 'repository'), array('cancel', 'moodle'), array('close', 'repository'),
|
||||
array('cleancache', 'repository'), array('copying', 'repository'), array('date', 'repository'), array('downloadsucc', 'repository'),
|
||||
@@ -459,6 +459,7 @@ class page_requirements_manager {
|
||||
array('renameto', 'repository'), array('fileexists', 'repository'),
|
||||
array('fileexistsdialogheader', 'repository'), array('fileexistsdialog_editor', 'repository'),
|
||||
array('fileexistsdialog_filemanager', 'repository'), array('uselatestfile', 'repository')
|
||||
array('lastmodified', 'moodle'), array('name', 'moodle'), array('type', 'repository')
|
||||
));
|
||||
break;
|
||||
case 'core_comment':
|
||||
|
||||
@@ -105,8 +105,8 @@ class repository_coursefiles extends repository {
|
||||
$encodedpath = base64_encode(serialize($params));
|
||||
$node = array(
|
||||
'title' => $child->get_visible_name(),
|
||||
'size' => 0,
|
||||
'date' => '',
|
||||
'datemodified' => $child->get_timemodified(),
|
||||
'datecreated' => $child->get_timecreated(),
|
||||
'path' => $encodedpath,
|
||||
'children'=>array(),
|
||||
'thumbnail' => $OUTPUT->pix_url('f/folder-32')->out(false)
|
||||
@@ -116,8 +116,11 @@ class repository_coursefiles extends repository {
|
||||
$encodedpath = base64_encode(serialize($child->get_params()));
|
||||
$node = array(
|
||||
'title' => $child->get_visible_name(),
|
||||
'size' => 0,
|
||||
'date' => '',
|
||||
'size' => $child->get_filesize(),
|
||||
'author' => $child->get_author(),
|
||||
'license' => $child->get_license(),
|
||||
'datemodified' => $child->get_timemodified(),
|
||||
'datecreated' => $child->get_timecreated(),
|
||||
'source'=> $encodedpath,
|
||||
'thumbnail' => $OUTPUT->pix_url(file_extension_icon($child->get_visible_name(), 32))->out(false)
|
||||
);
|
||||
|
||||
+89
-35
@@ -350,18 +350,18 @@ M.core_filepicker.init = function(Y, options) {
|
||||
build_tree: function(node, level) {
|
||||
var client_id = this.options.client_id;
|
||||
var dynload = this.active_repo.dynload;
|
||||
var info = {
|
||||
label:node.title,
|
||||
filename:node.title,
|
||||
source:node.source?node.source:'',
|
||||
thumbnail:node.thumbnail,
|
||||
path:node.path?node.path:''
|
||||
};
|
||||
if (node.dynamicLoadComplete) {
|
||||
info.dynamicLoadComplete = true;
|
||||
// prepare file name with icon
|
||||
var el = Y.Node.create('<div/>').setContent(M.core_filepicker.templates.listfilename);
|
||||
el.one('.fp-filename').setContent(node.title);
|
||||
if (node.icon && !node.children) {
|
||||
el.one('.fp-icon').appendChild(Y.Node.create('<img/>').set('src', node.icon));
|
||||
}
|
||||
var tmpNode = new YAHOO.widget.TextNode(info, level, false);
|
||||
tmpNode.repo_id = node.repo_id ? node.repo_id : this.active_repo.id;
|
||||
// create node
|
||||
var tmpNode = new YAHOO.widget.HTMLNode(el.getContent(), level, false);
|
||||
if (node.dynamicLoadComplete) {
|
||||
tmpNode.dynamicLoadComplete = true;
|
||||
}
|
||||
tmpNode.fileinfo = node;
|
||||
tmpNode.isLeaf = node.children ? false : true;
|
||||
if (!tmpNode.isLeaf) {
|
||||
if(node.expanded) {
|
||||
@@ -370,8 +370,7 @@ M.core_filepicker.init = function(Y, options) {
|
||||
if (dynload) {
|
||||
tmpNode.scope = this;
|
||||
}
|
||||
tmpNode.client_id = client_id;
|
||||
tmpNode.path = info.path;
|
||||
tmpNode.path = node.path ? node.path : '';
|
||||
for(var c in node.children) {
|
||||
this.build_tree(node.children[c], tmpNode);
|
||||
}
|
||||
@@ -385,10 +384,10 @@ M.core_filepicker.init = function(Y, options) {
|
||||
} else {
|
||||
this.viewbar_set_enabled(true);
|
||||
this.print_path();
|
||||
if (this.viewmode == 1) {
|
||||
this.view_as_icons();
|
||||
} else if (this.viewmode == 2) {
|
||||
if (this.viewmode == 2) {
|
||||
this.view_as_list();
|
||||
} else if (this.viewmode == 3) {
|
||||
this.view_as_table();
|
||||
} else {
|
||||
this.view_as_icons();
|
||||
}
|
||||
@@ -446,7 +445,7 @@ M.core_filepicker.init = function(Y, options) {
|
||||
var dynload = scope.active_repo.dynload;
|
||||
var list = this.filelist;
|
||||
scope.viewmode = 2;
|
||||
if (list && list.length==0 && (!this.filepath || !this.filepath.length)) {
|
||||
if (!list || list.length==0 && (!this.filepath || !this.filepath.length)) {
|
||||
this.display_error(M.str.repository.nofilesavailable, 'nofilesavailable');
|
||||
return;
|
||||
}
|
||||
@@ -485,7 +484,10 @@ M.core_filepicker.init = function(Y, options) {
|
||||
var root = scope.treeview.getRoot();
|
||||
while (root && root.children && root.children.length) {
|
||||
root = root.children[0];
|
||||
if (!root.isLeaf && root.expanded && root.path != mytreeel.path) {
|
||||
if (root.path == mytreeel.path) {
|
||||
root.origpath = scope.filepath;
|
||||
root.origlist = scope.filelist;
|
||||
} else if (!root.isLeaf && root.expanded) {
|
||||
scope.treeview_dynload(root, null);
|
||||
}
|
||||
}
|
||||
@@ -498,18 +500,14 @@ M.core_filepicker.init = function(Y, options) {
|
||||
}
|
||||
scope.treeview.subscribe('clickEvent', function(e){
|
||||
if(e.node.isLeaf){
|
||||
var fileinfo = {};
|
||||
fileinfo['title'] = e.node.data.filename;
|
||||
fileinfo['source'] = e.node.data.source;
|
||||
fileinfo['thumbnail'] = e.node.data.thumbnail;
|
||||
if (e.node.parent && e.node.parent.origpath) {
|
||||
// set the current path
|
||||
scope.filepath = e.node.parent.origpath;
|
||||
scope.filelist = e.node.parent.origlist;
|
||||
scope.print_path();
|
||||
}
|
||||
e.node.highlight(false)
|
||||
scope.select_file(fileinfo);
|
||||
e.node.highlight(false);
|
||||
scope.select_file(e.node.fileinfo);
|
||||
} else {
|
||||
// save current path and filelist (in case we want to jump to other viewmode)
|
||||
scope.filepath = e.node.origpath;
|
||||
@@ -524,7 +522,7 @@ M.core_filepicker.init = function(Y, options) {
|
||||
var list = this.filelist;
|
||||
this.viewmode = 1;
|
||||
|
||||
if (list && list.length==0) {
|
||||
if (!list || list.length==0) {
|
||||
this.display_error(M.str.repository.nofilesavailable, 'nofilesavailable');
|
||||
return;
|
||||
}
|
||||
@@ -576,28 +574,84 @@ M.core_filepicker.init = function(Y, options) {
|
||||
element.on('click', function(e, p) {
|
||||
e.preventDefault();
|
||||
if(dynload) {
|
||||
var params = {'path':p.path};
|
||||
scope.list(params);
|
||||
scope.list({'path':p.path});
|
||||
}else{
|
||||
this.filepath = p.path;
|
||||
this.filelist = p.children;
|
||||
this.view_files();
|
||||
}
|
||||
}, this, node);
|
||||
} else {
|
||||
var fileinfo = {};
|
||||
fileinfo['title'] = list[k].title;
|
||||
fileinfo['source'] = list[k].source;
|
||||
fileinfo['thumbnail'] = list[k].thumbnail;
|
||||
fileinfo['haslicense'] = list[k].haslicense?true:false;
|
||||
fileinfo['hasauthor'] = list[k].hasauthor?true:false;
|
||||
element.on('click', function(e, args) {
|
||||
e.preventDefault();
|
||||
this.select_file(args);
|
||||
}, this, fileinfo);
|
||||
}, this, list[k]);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
},
|
||||
view_as_table: function() {
|
||||
var list = this.filelist;
|
||||
var client_id = this.options.client_id;
|
||||
this.viewmode = 3;
|
||||
|
||||
if (!list || list.length==0) {
|
||||
this.display_error(M.str.repository.nofilesavailable, 'nofilesavailable');
|
||||
return;
|
||||
}
|
||||
var treeviewnode = Y.Node.create('<div/>').
|
||||
setAttrs({'class':'fp-tableview', id:'tableview-'+client_id});
|
||||
this.fpnode.one('.fp-content').setContent('').appendChild(treeviewnode);
|
||||
var formatValue = function (o){
|
||||
if (o.data[''+o.field+'_f_s']) { return o.data[''+o.field+'_f_s']; }
|
||||
else if (o.data[''+o.field+'_f']) { return o.data[''+o.field+'_f']; }
|
||||
else if (o.value) { return o.value; }
|
||||
else { return ''; }
|
||||
};
|
||||
var formatTitle = function(o) {
|
||||
var el = Y.Node.create('<div/>').setContent(M.core_filepicker.templates.listfilename);
|
||||
el.one('.fp-filename').setContent(o.value);
|
||||
el.one('.fp-icon').appendChild(Y.Node.create('<img/>').set('src', o.data['icon']));
|
||||
return el.getContent();
|
||||
}
|
||||
|
||||
var cols = [
|
||||
{key: "title", label: M.str.moodle.name, sortable: true, formatter: formatTitle},
|
||||
{key: "datemodified", label: M.str.moodle.lastmodified, sortable: true, formatter: formatValue},
|
||||
{key: "size", label: M.str.repository.size, sortable: true, formatter: formatValue},
|
||||
{key: "type", label: M.str.repository.type, sortable: true}
|
||||
];
|
||||
var table = new Y.DataTable.Base({
|
||||
columnset: cols,
|
||||
recordset: list
|
||||
});
|
||||
// TODO allow sorting only if repository allows it, remember last sort order
|
||||
table.plug(Y.Plugin.DataTableSort/*, { lastSortedBy: {key: "title", dir: "asc"} }*/);
|
||||
|
||||
table.render('#tableview-'+client_id);
|
||||
table.delegate('click', function (e) {
|
||||
var rs = table.get('recordset');
|
||||
var id = e.currentTarget.get('id'), len = rs.size(), i;
|
||||
for (i = 0; i < len; ++i) {
|
||||
var record = rs.item(i);
|
||||
if (record.get('id') == id) {
|
||||
// we found the clicked record
|
||||
var data = record.getValue();
|
||||
if (data.children) {
|
||||
if (this.active_repo.dynload) {
|
||||
this.list({'path':data.path});
|
||||
} else {
|
||||
this.filepath = data.path;
|
||||
this.filelist = data.children;
|
||||
this.view_files();
|
||||
}
|
||||
} else {
|
||||
this.select_file(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'tr', this);
|
||||
},
|
||||
select_file: function(args) {
|
||||
this.selectui.show();
|
||||
var client_id = this.options.client_id;
|
||||
@@ -1391,7 +1445,7 @@ M.core_filepicker.init = function(Y, options) {
|
||||
this.viewbar_set_enabled(false);
|
||||
var repository_id = Y.Cookie.get('recentrepository');
|
||||
this.viewmode = Y.Cookie.get('recentviewmode', Number);
|
||||
if (this.viewmode != 1 && this.viewmode != 2 && this.viewmode != 3) {
|
||||
if (this.viewmode != 2 && this.viewmode != 3) {
|
||||
this.viewmode = 1;
|
||||
}
|
||||
if (this.options.repositories[repository_id]) {
|
||||
|
||||
@@ -30,6 +30,7 @@ $string['filesystem:view'] = 'View file system repository';
|
||||
$string['information'] = 'These folders are within the <b>{$a}</b> directory.';
|
||||
$string['invalidpath'] = 'Invalid root path';
|
||||
$string['path'] = 'Select a subdirectory';
|
||||
$string['root'] = 'Root';
|
||||
$string['nosubdir'] = 'You need to create at least one folder inside the <b>{$a}</b> directory so you can select it here.';
|
||||
$string['pluginname_help'] = 'Create repository from local directory';
|
||||
$string['pluginname'] = 'File system';
|
||||
|
||||
@@ -70,7 +70,7 @@ class repository_filesystem extends repository {
|
||||
$list['list'] = array();
|
||||
// process breacrumb trail
|
||||
$list['path'] = array(
|
||||
array('name'=>'Root', 'path'=>'')
|
||||
array('name'=>get_string('root', 'repository_filesystem'), 'path'=>'')
|
||||
);
|
||||
$trail = '';
|
||||
if (!empty($path)) {
|
||||
@@ -99,7 +99,8 @@ class repository_filesystem extends repository {
|
||||
'title' => $file,
|
||||
'source' => $path.'/'.$file,
|
||||
'size' => filesize($this->root_path.$file),
|
||||
'date' => time(),
|
||||
'datecreated' => filectime($this->root_path.$file),
|
||||
'datemodified' => filemtime($this->root_path.$file),
|
||||
'thumbnail' => $OUTPUT->pix_url(file_extension_icon($this->root_path.$file, 32))->out(false)
|
||||
);
|
||||
} else {
|
||||
@@ -111,6 +112,8 @@ class repository_filesystem extends repository {
|
||||
$list['list'][] = array(
|
||||
'title' => $file,
|
||||
'children' => array(),
|
||||
'datecreated' => filectime($this->root_path.$file),
|
||||
'datemodified' => filemtime($this->root_path.$file),
|
||||
'thumbnail' => $OUTPUT->pix_url('f/folder-32')->out(false),
|
||||
'path' => $current_path
|
||||
);
|
||||
|
||||
+54
-1
@@ -1780,7 +1780,60 @@ abstract class repository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search files in repository.
|
||||
* Prepares list of files before passing it to AJAX, makes sure data is in the correct
|
||||
* format and stores formatted dates.
|
||||
*
|
||||
* @param array $listing result of get_listing() or search()
|
||||
* @return array
|
||||
*/
|
||||
public static function prepare_listing($listing) {
|
||||
global $OUTPUT;
|
||||
if (!is_array($listing) || !isset($listing['list'])) {
|
||||
return $listing;
|
||||
}
|
||||
$len = count($listing['list']);
|
||||
for ($i=0; $i<$len; $i++) {
|
||||
if (isset($listing['list'][$i]['size'])) {
|
||||
$listing['list'][$i]['size'] = (int)$listing['list'][$i]['size'];
|
||||
$listing['list'][$i]['size_f'] = display_size($listing['list'][$i]['size']);
|
||||
}
|
||||
foreach (array('date', 'datemodified', 'datecreated') as $key) {
|
||||
if (!isset($listing['list'][$i][$key]) && isset($listing['list'][$i]['date'])) {
|
||||
$listing['list'][$i][$key] = $listing['list'][$i]['date'];
|
||||
}
|
||||
if (isset($listing['list'][$i][$key])) {
|
||||
// must be UNIX timestamp
|
||||
$listing['list'][$i][$key] = (int)$listing['list'][$i][$key];
|
||||
if (!$listing['list'][$i][$key]) {
|
||||
unset($listing['list'][$i][$key]);
|
||||
} else {
|
||||
$listing['list'][$i][$key.'_f'] = userdate($listing['list'][$i][$key], get_string('strftimedatetime', 'langconfig'));
|
||||
$listing['list'][$i][$key.'_f_s'] = userdate($listing['list'][$i][$key], get_string('strftimedatetimeshort', 'langconfig'));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isset($listing['list'][$i]['type']) && !array_key_exists('children', $listing['list'][$i]) && isset($listing['list'][$i]['title'])) {
|
||||
$mimetype = mimeinfo('type', $listing['list'][$i]['title']);
|
||||
if (get_string_manager()->string_exists($mimetype, 'mimetypes')) {
|
||||
$mimetype = get_string($mimetype, 'mimetypes');
|
||||
}
|
||||
$listing['list'][$i]['type'] = $mimetype;
|
||||
}
|
||||
if (!isset($listing['list'][$i]['icon']) && isset($listing['list'][$i]['title'])) {
|
||||
if (array_key_exists('children', $listing['list'][$i])) {
|
||||
$listing['list'][$i]['icon'] = $OUTPUT->pix_url('f/folder')->out(false);
|
||||
} else {
|
||||
$listing['list'][$i]['icon'] = $OUTPUT->pix_url('f/'.mimeinfo('icon', $listing['list'][$i]['title']))->out(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $listing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search files in repository
|
||||
* When doing global search, $search_text will be used as
|
||||
* keyword.
|
||||
*
|
||||
* @param string $search_text search key word
|
||||
* @param int $page page
|
||||
|
||||
@@ -208,13 +208,17 @@ class repository_local_file {
|
||||
$encodedpath = base64_encode(serialize($this->fileinfo->get_params()));
|
||||
$node = array(
|
||||
'title' => $this->fileinfo->get_visible_name(),
|
||||
'size' => 0,
|
||||
'date' => '');
|
||||
'datemodified' => $this->fileinfo->get_timemodified(),
|
||||
'datecreated' => $this->fileinfo->get_timecreated()
|
||||
);
|
||||
if ($this->isdir) {
|
||||
$node['path'] = $encodedpath;
|
||||
$node['thumbnail'] = $OUTPUT->pix_url('f/folder-32')->out(false);
|
||||
$node['children'] = array();
|
||||
} else {
|
||||
$node['size'] = $this->fileinfo->get_filesize();
|
||||
$node['author'] = $this->fileinfo->get_author();
|
||||
$node['license'] = $this->fileinfo->get_license();
|
||||
$node['source'] = $encodedpath;
|
||||
$node['thumbnail'] = $OUTPUT->pix_url(file_extension_icon($node['title'], 32))->out(false);
|
||||
}
|
||||
|
||||
@@ -115,14 +115,20 @@ class repository_recent extends repository {
|
||||
|
||||
try {
|
||||
foreach ($files as $file) {
|
||||
$params = base64_encode(serialize($file));
|
||||
// Check that file exists and accessible
|
||||
$filesize = $this->get_file_size($params);
|
||||
if (!empty($filesize)) {
|
||||
// Check that file exists and accessible, retrieve size/date info
|
||||
$browser = get_file_browser();
|
||||
$context = get_context_instance_by_id($file['contextid']);
|
||||
$fileinfo = $browser->get_file_info($context, $file['component'],
|
||||
$file['filearea'], $file['itemid'], $file['filepath'], $file['filename']);
|
||||
if ($fileinfo) {
|
||||
$params = base64_encode(serialize($file));
|
||||
$node = array(
|
||||
'title' => $file['filename'],
|
||||
'size' => $filesize,
|
||||
'date' => '',
|
||||
'size' => $fileinfo->get_filesize(),
|
||||
'datemodified' => $fileinfo->get_timemodified(),
|
||||
'datecreated' => $fileinfo->get_timecreated(),
|
||||
'author' => $fileinfo->get_author(),
|
||||
'license' => $fileinfo->get_license(),
|
||||
'source'=> $params,
|
||||
'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file['filename'], 32))->out(false),
|
||||
);
|
||||
|
||||
@@ -144,6 +144,25 @@ class core_repository_renderer extends plugin_renderer_base {
|
||||
return preg_replace('/\{\!\}/', '', $rv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for displaying file name in 'table view' and 'tree view' modes.
|
||||
*
|
||||
* content of the element with class 'fp-icon' will be replaced with an appropriate img;
|
||||
*
|
||||
* content of element with class 'fp-filename' will be replaced with filename supplied by
|
||||
* repository;
|
||||
*
|
||||
* Note that tree view and table view are the YUI widgets and therefore there are no
|
||||
* other templates. The widgets will be wrapped in <div> with class fp-treeview or
|
||||
* fp-tableview (respectfully).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function js_template_listfilename() {
|
||||
$rv = '<span class="{!}fp-icon"></span> <span class="{!}fp-filename"></span>';
|
||||
return preg_replace('/\{\!\}/', '', $rv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Template for window appearing to select a file.
|
||||
*
|
||||
|
||||
@@ -142,7 +142,7 @@ switch ($action) {
|
||||
case 'signin':
|
||||
case 'list':
|
||||
if ($repo->check_login()) {
|
||||
$listing = $repo->get_listing($req_path, $page);
|
||||
$listing = repository::prepare_listing($repo->get_listing($req_path, $page));
|
||||
$listing['repo_id'] = $repo_id;
|
||||
echo json_encode($listing);
|
||||
break;
|
||||
@@ -166,7 +166,7 @@ switch ($action) {
|
||||
echo json_encode($search_form);
|
||||
break;
|
||||
case 'search':
|
||||
$search_result = $repo->search($search_text, (int)$page);
|
||||
$search_result = repository::prepare_listing($repo->search($search_text, (int)$page));
|
||||
$search_result['repo_id'] = $repo_id;
|
||||
$search_result['issearchresult'] = true;
|
||||
echo json_encode($search_result);
|
||||
|
||||
@@ -96,8 +96,8 @@ class repository_user extends repository {
|
||||
$encodedpath = base64_encode(serialize($child->get_params()));
|
||||
$node = array(
|
||||
'title' => $child->get_visible_name(),
|
||||
'size' => 0,
|
||||
'date' => '',
|
||||
'datemodified' => $child->get_timemodified(),
|
||||
'datecreated' => $child->get_timecreated(),
|
||||
'path' => $encodedpath,
|
||||
'children'=>array(),
|
||||
'thumbnail' => $OUTPUT->pix_url('f/folder-32')->out(false)
|
||||
@@ -107,8 +107,11 @@ class repository_user extends repository {
|
||||
$encodedpath = base64_encode(serialize($child->get_params()));
|
||||
$node = array(
|
||||
'title' => $child->get_visible_name(),
|
||||
'size' => 0,
|
||||
'date' => '',
|
||||
'size' => $child->get_filesize(),
|
||||
'datemodified' => $child->get_timemodified(),
|
||||
'datecreated' => $child->get_timecreated(),
|
||||
'author' => $child->get_author(),
|
||||
'license' => $child->get_license(),
|
||||
'source'=> $encodedpath,
|
||||
'thumbnail' => $OUTPUT->pix_url(file_extension_icon($child->get_visible_name(), 32))->out(false)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user