mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-27 17:18:54 +01:00
Merge branch 'master' into web-components
This commit is contained in:
commit
2545c8fde1
@ -176,7 +176,7 @@ class admin_mail
|
||||
* Step 1: IMAP account
|
||||
*
|
||||
* @param array $content
|
||||
* @param type $msg
|
||||
* @param string $msg
|
||||
*/
|
||||
public function add(array $content=array(), $msg='', $msg_type='success')
|
||||
{
|
||||
|
@ -338,7 +338,7 @@
|
||||
<row class="emailadmin_no_user dialogHeader2">
|
||||
<description for="account_id" value="Valid for"/>
|
||||
<hbox>
|
||||
<select type="select-account" id="account_id" onchange="app.admin.account_hide_not_applying" options="Everyone,both" multiple="dynamic"/>
|
||||
<select-account account_type="both" id="account_id" onchange="app.admin.account_hide_not_applying" empty_label="Everyone" multiple="dynamic"/>
|
||||
<buttononly label="Select multiple" id="button[multiple]" onclick="app.admin.edit_multiple" options="users"/>
|
||||
<checkbox label="account editable by user" id="acc_user_editable"/>
|
||||
</hbox>
|
||||
|
@ -14,7 +14,7 @@ $setup_info['api']['title'] = 'EGroupware API';
|
||||
$setup_info['api']['version'] = '21.1.001';
|
||||
$setup_info['api']['versions']['current_header'] = '1.29';
|
||||
// maintenance release in sync with changelog in doc/rpm-build/debian.changes
|
||||
$setup_info['api']['versions']['maintenance_release'] = '21.1.20210923';
|
||||
$setup_info['api']['versions']['maintenance_release'] = '21.1.20211130';
|
||||
$setup_info['api']['enable'] = 3;
|
||||
$setup_info['api']['app_order'] = 1;
|
||||
$setup_info['api']['license'] = 'GPL';
|
||||
|
@ -764,7 +764,7 @@ class Contacts extends Contacts\Storage
|
||||
$data[$name] = DateTime::server2user($data[$name], $date_format);
|
||||
}
|
||||
}
|
||||
$data['photo'] = $this->photo_src($data['id'],!empty($data['jpegphoto']) || (($data['files']??0) & self::FILES_BIT_PHOTO), '', $data['etag'] ?? null);
|
||||
$data['photo'] = $this->photo_src($data['id'] ?? null,!empty($data['jpegphoto']) || (($data['files']??0) & self::FILES_BIT_PHOTO), '', $data['etag'] ?? null);
|
||||
|
||||
// set freebusy_uri for accounts
|
||||
if (empty($data['freebusy_uri']) && empty($data['owner']) && !empty($data['account_id']) && empty($GLOBALS['egw_setup']))
|
||||
|
@ -482,13 +482,13 @@ class Sql extends Api\Storage
|
||||
$owner = isset($filter['owner']) ? $filter['owner'] : (isset($criteria['owner']) ? $criteria['owner'] : null);
|
||||
|
||||
// fix cat_id criteria to search in comma-separated multiple cats and return subcats
|
||||
if (is_array($criteria) && ($cats = $criteria['cat_id']))
|
||||
if (is_array($criteria) && !empty($criteria['cat_id']))
|
||||
{
|
||||
$criteria = array_merge($criteria, $this->_cat_search($criteria['cat_id']));
|
||||
unset($criteria['cat_id']);
|
||||
}
|
||||
// fix cat_id filter to search in comma-separated multiple cats and return subcats
|
||||
if (($cats = $filter['cat_id']))
|
||||
if (!empty($filter['cat_id']))
|
||||
{
|
||||
if ($filter['cat_id'][0] == '!')
|
||||
{
|
||||
@ -680,7 +680,7 @@ class Sql extends Api\Storage
|
||||
{
|
||||
$extra_cols[$key] = "$shared_with AS shared_with";
|
||||
}
|
||||
switch ((string)$filter['shared_with'])
|
||||
switch ($filter['shared_with'] ?? '')
|
||||
{
|
||||
case '': // filter not set
|
||||
break;
|
||||
|
@ -655,7 +655,7 @@ class Storage
|
||||
//error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',".array2string($start).','.array2string($filter,true).",'$join')");
|
||||
|
||||
// Handle 'None' country option
|
||||
if(is_array($filter) && $filter['adr_one_countrycode'] == '-custom-')
|
||||
if(is_array($filter) && isset($filter['adr_one_countrycode']) && $filter['adr_one_countrycode'] === '-custom-')
|
||||
{
|
||||
$filter[] = 'adr_one_countrycode IS NULL';
|
||||
unset($filter['adr_one_countrycode']);
|
||||
|
@ -1630,9 +1630,10 @@ class Db
|
||||
$not_null = is_array($column_definitions) && isset($column_definitions[$col]['nullable']) ? !$column_definitions[$col]['nullable'] : false;
|
||||
|
||||
$maxlength = null;
|
||||
if ($truncate_varchar)
|
||||
if ($truncate_varchar && !is_int($col) && isset($column_definitions[$col]) &&
|
||||
in_array($column_definitions[$col]['type'], ['varchar','ascii']))
|
||||
{
|
||||
$maxlength = in_array($column_definitions[$col]['type'], array('varchar','ascii')) ? $column_definitions[$col]['precision'] : null;
|
||||
$maxlength = $column_definitions[$col]['precision'];
|
||||
}
|
||||
// dont use IN ( ), if there's only one value, it's slower for MySQL
|
||||
if (is_array($data) && count($data) <= 1)
|
||||
|
@ -67,6 +67,10 @@ class Backup
|
||||
* @var string|boolean
|
||||
*/
|
||||
var $egw_tables = false;
|
||||
/**
|
||||
* Regular expression to identify a Guacamole table OR view
|
||||
*/
|
||||
const GUACAMOLE_REGEXP = '/^guacamole_/';
|
||||
/**
|
||||
* Backup directory.
|
||||
*
|
||||
@ -360,6 +364,8 @@ class Backup
|
||||
if (substr($this->db->Type,0,5) == 'mysql')
|
||||
{
|
||||
$this->db->query("SET SESSION sql_mode=(SELECT REPLACE(REPLACE(@@sql_mode,'STRICT_ALL_TABLES',''),'STRICT_TRANS_TABLES',''))", __LINE__, __FILE__);
|
||||
// disable foreign key checks, in case Guacamole is installed
|
||||
$this->db->query('SET FOREIGN_KEY_CHECKS = 0');
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -371,7 +377,9 @@ class Backup
|
||||
foreach($this->adodb->MetaTables('TABLES') as $table)
|
||||
{
|
||||
if ($this->system_tables && preg_match($this->system_tables,$table) ||
|
||||
$this->egw_tables && !preg_match($this->egw_tables,$table))
|
||||
$this->egw_tables && !preg_match($this->egw_tables,$table) ||
|
||||
// do NOT drop Guacamole tables and views
|
||||
preg_match(self::GUACAMOLE_REGEXP, $table))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -513,7 +521,7 @@ class Backup
|
||||
|
||||
if (substr($line,0,9) == 'version: ')
|
||||
{
|
||||
// currenty not used: $api_version = trim(substr($line,9));
|
||||
// currently not used: $api_version = trim(substr($line,9));
|
||||
continue;
|
||||
}
|
||||
if (substr($line,0,9) == 'charset: ')
|
||||
@ -536,6 +544,12 @@ class Backup
|
||||
$this->schemas = json_php_unserialize(trim(substr($line,8)));
|
||||
foreach($this->schemas as $table_name => $schema)
|
||||
{
|
||||
// do NOT create GUACAMOLE tables, just truncate them (as we have no abstraction to create the foreign keys)
|
||||
if (preg_match(self::GUACAMOLE_REGEXP, $table_name))
|
||||
{
|
||||
$this->db->query('TRUNCATE TABLE '.$this->db->name_quote($table_name));
|
||||
continue;
|
||||
}
|
||||
// if column is longtext in current schema, convert text to longtext, in case user already updated column
|
||||
foreach($schema['fd'] as $col => &$def)
|
||||
{
|
||||
@ -566,7 +580,7 @@ class Backup
|
||||
{
|
||||
if ($data['type'] == 'blob') $blobs[] = $col;
|
||||
}
|
||||
// check if we have an old PostgreSQL backup useing 't'/'f' for bool values
|
||||
// check if we have an old PostgreSQL backup using 't'/'f' for bool values
|
||||
// --> convert them to MySQL and our new PostgreSQL format of 1/0
|
||||
$bools = array();
|
||||
foreach($this->schemas[$table]['fd'] as $col => $def)
|
||||
|
@ -119,22 +119,22 @@ class Etemplate extends Etemplate\Widget\Template
|
||||
{
|
||||
if (!empty($extra['data']) && is_array($extra['data']))
|
||||
{
|
||||
$content = array_merge($content, $extra['data']);
|
||||
$content = array_merge_recursive($content, $extra['data']);
|
||||
}
|
||||
|
||||
if (!empty($extra['preserve']) && is_array($extra['preserve']))
|
||||
{
|
||||
$preserv = array_merge($preserv, $extra['preserve']);
|
||||
$preserv = array_merge_recursive($preserv, $extra['preserve']);
|
||||
}
|
||||
|
||||
if (!empty($extra['readonlys']) && is_array($extra['readonlys']))
|
||||
{
|
||||
$readonlys = array_merge($readonlys, $extra['readonlys']);
|
||||
$readonlys = array_merge_recursive($readonlys, $extra['readonlys']);
|
||||
}
|
||||
|
||||
if (!empty($extra['sel_options']) && is_array($extra['sel_options']))
|
||||
{
|
||||
$sel_options = array_merge($sel_options, $extra['sel_options']);
|
||||
$sel_options = array_merge_recursive($sel_options, $extra['sel_options']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -569,7 +569,10 @@ class Widget
|
||||
$types = $paramType instanceof \ReflectionUnionType
|
||||
? $paramType->getTypes()
|
||||
: [$paramType];
|
||||
if(in_array('array', array_map(fn(\ReflectionNamedType $t) => $t->getName(), $types)) && !is_array($params[$index]))
|
||||
if(in_array('array', array_map(static function(\ReflectionNamedType $t)
|
||||
{
|
||||
return $t->getName();
|
||||
}, $types)) && !is_array($params[$index]))
|
||||
{
|
||||
error_log("$method_name expects an array for {$param->getPosition()}: {$param->getName()}");
|
||||
$params[$index] = (array)$params[$index];
|
||||
|
@ -75,11 +75,32 @@ class File extends Etemplate\Widget
|
||||
return;
|
||||
}
|
||||
|
||||
if (!($template = Template::instance(self::$request->template['name'], self::$request->template['template_set'],
|
||||
self::$request->template['version'], self::$request->template['load_via'])))
|
||||
{
|
||||
// Can't use callback
|
||||
error_log("Could not get template for file upload, callback skipped");
|
||||
try {
|
||||
if (!($template = Template::instance(self::$request->template['name'], self::$request->template['template_set'],
|
||||
self::$request->template['version'], self::$request->template['load_via'])))
|
||||
{
|
||||
// Can't use callback
|
||||
error_log("Could not get template for file upload, callback skipped");
|
||||
}
|
||||
}
|
||||
catch (\Error $e) {
|
||||
// retry 3 times, in case the problem (Call to undefined method EGroupware\Api\Etemplate\Widget\Vfs::set_attrs()) is caused by something internal in PHP 8.0
|
||||
if (!isset($_REQUEST['retry']) || $_REQUEST['retry'] < 3)
|
||||
{
|
||||
$url = Api\Header\Http::schema().'://'.Api\Header\Http::host().$_SERVER['REQUEST_URI'];
|
||||
if (strpos($url, '&retry=') === false)
|
||||
{
|
||||
$url .= '&retry=1';
|
||||
}
|
||||
else
|
||||
{
|
||||
$url = preg_replace('/&retry=\d+/', '&retry='.($_REQUEST['retry']+1), $url);
|
||||
}
|
||||
header('Location: '.$url);
|
||||
http_response_code(307);
|
||||
exit;
|
||||
}
|
||||
throw new \Error('Error instantiating template '.json_encode(self::$request->template).', $_REQUEST='.json_encode($_REQUEST).': '.$e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
$file_data = array();
|
||||
|
@ -578,9 +578,9 @@ class calendar_ui
|
||||
// ignore failed discovery
|
||||
unset($e);
|
||||
}
|
||||
if ($GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir'])
|
||||
if($GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir'])
|
||||
{
|
||||
$sel_options['merge'] = calendar_merge::get_documents($GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir'], '', null,'calendar');
|
||||
$sel_options['merge'] = calendar_merge::get_documents($GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir'], '', null, 'calendar');
|
||||
|
||||
}
|
||||
else
|
||||
@ -588,6 +588,22 @@ class calendar_ui
|
||||
$readonlys['merge'] = true;
|
||||
}
|
||||
|
||||
// Add integration UI into sidemenu
|
||||
$integration_data = Api\Hooks::process(array('location' => 'calendar_search_union'));
|
||||
foreach($integration_data as $app => $app_hooks)
|
||||
{
|
||||
foreach($app_hooks as $data)
|
||||
{
|
||||
// App might have multiple hooks, let it specify something else
|
||||
$app = $data['selects']['app'] ?: $app;
|
||||
|
||||
if(array_key_exists('sidebox_template', $data))
|
||||
{
|
||||
$cont['integration'][] = ['template' => $data['sidebox_template'], 'app' => $app];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebox?
|
||||
$sidebox->exec('calendar.calendar_ui.sidebox_etemplate', $cont, $sel_options, $readonlys);
|
||||
}
|
||||
|
@ -861,10 +861,14 @@ export class CalendarApp extends EgwApp
|
||||
|
||||
if(action.checked)
|
||||
{
|
||||
integration_preference.push(app);
|
||||
if(integration_preference.indexOf(app) === -1)
|
||||
{
|
||||
integration_preference.push(app);
|
||||
}
|
||||
|
||||
// After the preference change is done, get new info which should now include the app
|
||||
callback = callback ? callback : function() {
|
||||
callback = callback ? callback : function()
|
||||
{
|
||||
this._fetch_data(this.state);
|
||||
}.bind(this);
|
||||
}
|
||||
|
@ -14,27 +14,52 @@ Egroupware
|
||||
<overlay>
|
||||
<template id="calendar.sidebox">
|
||||
<vbox parent_node="calendar-et2_target">
|
||||
<buttononly id="header_today" label="•" icon="nope" onclick="
|
||||
<buttononly id="header_today" label="•" icon="nope" onclick="
|
||||
var tempDate = new Date();
|
||||
var today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(),0,-tempDate.getTimezoneOffset(),0);
|
||||
var change = {date: today.toJSON()};
|
||||
app.calendar.update_state(change);
|
||||
widget.getRoot().getWidgetById('date').set_value(today);
|
||||
return false;"/>
|
||||
<buttononly id="header_go" label="↵" icon="nope" class="ui-corner-all" onclick="var change = {date: widget.btn.attr('data-date')}; if ( app.calendar.state.view == 'listview') {change.filter='month';} else if (app.calendar.state.view == 'planner') {} else {change.view = 'month';}app.calendar.update_state(change);" />
|
||||
<date id="date" class="et2_fullWidth" inline="true" onchange="var view_change = app.calendar.sidebox_changes_views.indexOf(app.calendar.state.view);
|
||||
<buttononly id="header_go" label="↵" icon="nope" class="ui-corner-all"
|
||||
onclick="var change = {date: widget.btn.attr('data-date')}; if ( app.calendar.state.view == 'listview') {change.filter='month';} else if (app.calendar.state.view == 'planner') {} else {change.view = 'month';}app.calendar.update_state(change);"/>
|
||||
<date id="date" class="et2_fullWidth" inline="true" onchange="var view_change = app.calendar.sidebox_changes_views.indexOf(app.calendar.state.view);
|
||||
var update = {date:widget.getValue()};
|
||||
if(view_change >= 0) {update.view = app.calendar.sidebox_changes_views[view_change ? view_change - 1 : view_change];} else if (app.calendar.state.view == 'listview') {update.filter = 'after';} else if (app.calendar.state.view =='planner') { update.planner_view = 'day'; } app.calendar.update_state(update);"/>
|
||||
<textbox type="hidden" id="first"/>
|
||||
<textbox type="hidden" id="last"/>
|
||||
<hrule/>
|
||||
<select-cat id="cat_id" empty_label="All categories" width="86%" onchange="app.calendar.update_state({cat_id: widget.getValue()});" expand_multiple_rows="4"/>
|
||||
<select id="status_filter" no_lang="true" class="et2_fullWidth" onchange="app.calendar.update_state({status_filter: widget.getValue()});"/>
|
||||
<hrule/>
|
||||
<calendar-owner id="owner" class="et2_fullWidth" onchange="app.calendar.update_state({owner: widget.getValue()}); return false;" multiple="true" allowFreeEntries="false" autocomplete_params="{"checkgrants": true}"/>
|
||||
<hrule/>
|
||||
<select id="merge" empty_label="Insert in document" onchange="app.calendar.sidebox_merge" class="et2_fullWidth"/>
|
||||
</vbox>
|
||||
<iframe id="iframe" width="100%" height="100%"/>
|
||||
</template>
|
||||
<textbox type="hidden" id="first"/>
|
||||
<textbox type="hidden" id="last"/>
|
||||
<hrule/>
|
||||
<select-cat id="cat_id" empty_label="All categories" width="86%"
|
||||
onchange="app.calendar.update_state({cat_id: widget.getValue()});" expand_multiple_rows="4"/>
|
||||
<select id="status_filter" no_lang="true" class="et2_fullWidth"
|
||||
onchange="app.calendar.update_state({status_filter: widget.getValue()});"/>
|
||||
<hrule/>
|
||||
<calendar-owner id="owner" class="et2_fullWidth"
|
||||
onchange="app.calendar.update_state({owner: widget.getValue()}); return false;"
|
||||
multiple="true" allowFreeEntries="false"
|
||||
autocomplete_params="{"checkgrants": true}"/>
|
||||
<hrule/>
|
||||
<select id="merge" empty_label="Insert in document" onchange="app.calendar.sidebox_merge"
|
||||
class="et2_fullWidth"/>
|
||||
<box>
|
||||
<grid id="integration" disabled="!@integration" width="100%">
|
||||
<columns>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<template id="$row_cont[template]" width="100%" content="$row_cont[app]"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</box>
|
||||
</vbox>
|
||||
<iframe id="iframe" width="100%" height="100%"/>
|
||||
<styles>
|
||||
#calendar-sidebox_integration: {
|
||||
display: table;
|
||||
width: 100%
|
||||
}
|
||||
</styles>
|
||||
</template>
|
||||
</overlay>
|
||||
|
@ -118,7 +118,7 @@
|
||||
"egroupware/tracker": "self.version",
|
||||
"egroupware/util": "^2.6.2",
|
||||
"egroupware/webdav": "^v0.3.2",
|
||||
"egroupware/z-push-dev": "^2.5",
|
||||
"egroupware/z-push-dev": "2.5.0.1",
|
||||
"giggsey/libphonenumber-for-php": "^8.12",
|
||||
"npm-asset/as-jqplot": "1.0.*",
|
||||
"npm-asset/gridster": "0.5.*",
|
||||
|
14
composer.lock
generated
14
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "beeabf26cf7c537dea75e236217d44c6",
|
||||
"content-hash": "393ec43e69c00afa115414c62789e29a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adldap2/adldap2",
|
||||
@ -207,7 +207,7 @@
|
||||
"version": "v2.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fengyuanchen/cropper.git",
|
||||
"url": "git@github.com:fengyuanchen/cropper.git",
|
||||
"reference": "30c58b29ee21010e17e58ebab165fbd84285c685"
|
||||
},
|
||||
"dist": {
|
||||
@ -305,7 +305,7 @@
|
||||
"version": "1.12.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jquery/jquery-dist.git",
|
||||
"url": "git@github.com:jquery/jquery-dist.git",
|
||||
"reference": "5e89585e0121e72ff47de177c5ef604f3089a53d"
|
||||
},
|
||||
"dist": {
|
||||
@ -1924,16 +1924,16 @@
|
||||
},
|
||||
{
|
||||
"name": "egroupware/z-push-dev",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/EGroupware/z-push.git",
|
||||
"reference": "32da00e1024038a8f57c8a185c671179c3922ebe"
|
||||
"reference": "7774018b19b5b55e24dbd1107693496597ab70ee"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/EGroupware/z-push/zipball/32da00e1024038a8f57c8a185c671179c3922ebe",
|
||||
"reference": "32da00e1024038a8f57c8a185c671179c3922ebe",
|
||||
"url": "https://api.github.com/repos/EGroupware/z-push/zipball/7774018b19b5b55e24dbd1107693496597ab70ee",
|
||||
"reference": "7774018b19b5b55e24dbd1107693496597ab70ee",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1,3 +1,29 @@
|
||||
egroupware-docker (21.1.20211130) hardy; urgency=low
|
||||
|
||||
* PHP 8.0: tons of fixes to support 8.0, this is probably the last container using PHP 7.4 by default
|
||||
* Addressbook: new REST API for contacts https://github.com/EGroupware/egroupware/tree/master/doc/REST-CalDAV-CardDAV
|
||||
* LDAP/Addressbook: fix region contains for given country invalid value gives an LDAP error on update
|
||||
* Admin/Filemanager: correctly encode user "WORKGROUP\$user" for SMB mounts and do NOT require mountpoints to exist
|
||||
* Filemanager: fix not working variables eg. $user in GUI mount (Admin > Filemanager)
|
||||
* Filemanager: fix video controller not working in filemanager gallery
|
||||
* Filemanager: add action to unlock files
|
||||
* Filemanager: fix super user could not remove other users' subscriptions
|
||||
* Filemanager: add actions to convert editable files to PDF or PNG and a checkbox to merge file as PDF
|
||||
* Collabora: merge placeholder dialogs
|
||||
* All apps: add preference to set directory and filename of merged documents using placeholders
|
||||
* Calendar: fix changing the recurrence end date did not add/remove the events in the UI
|
||||
* InfoLog: fix not working overwrite check (optimistic locking) plus incrementing etag
|
||||
* Mail: make sure pressing [del] key twice in a row does not delete the first row on the second press
|
||||
* Mail: add set flags action into mail filters
|
||||
* Mail: implements date extension for vacation rule. None imap admin user can also set vacation rule by date.
|
||||
* Kanban: fix deleting card did not delete link to the board (includes a DB update to remove orphans from links)
|
||||
* Resources: add inventory number to resource list columns
|
||||
* smallPART/PostgreSQL: fix SQL error when opening a course
|
||||
* smallPART/PostgreSQL: fix SQL error during update (you need to restore egw_smallpart* tables AND set egw_applications.app_version='21.1')
|
||||
* API: update jQuery-ui to 1.13.0 and TinyMCE to 5.10.1
|
||||
|
||||
-- Ralf Becker <rb@egroupware.org> Tue, 30 Nov 2021 09:11:56 +0100
|
||||
|
||||
egroupware-docker (21.1.20210923) hardy; urgency=low
|
||||
|
||||
* smallPART: many new features and improvements for the new semester:
|
||||
|
Loading…
Reference in New Issue
Block a user