Merge branch 'master' into web-components

This commit is contained in:
nathan 2021-08-12 10:35:46 -06:00
commit fac1dfb8d4
48 changed files with 2742 additions and 4242 deletions

2
.gitignore vendored
View File

@ -65,6 +65,8 @@ status/
smallpart/
swoolepush/
webauthn/
*/js/*.map
*/js/app.min.js
addressbook/js/app.js
admin/js/app.js
api/js/etemplate/*.js

View File

@ -41,7 +41,7 @@ module.exports = function (grunt) {
files: {
"pixelegg/css/pixelegg.min.css": [
"api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css",
"node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -59,7 +59,7 @@ module.exports = function (grunt) {
],
"pixelegg/css/mobile.min.css": [
"api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css",
"node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -77,7 +77,7 @@ module.exports = function (grunt) {
],
"pixelegg/mobile/fw_mobile.min.css": [
"api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css",
"node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -94,7 +94,7 @@ module.exports = function (grunt) {
],
"pixelegg/css/monochrome.min.css": [
"api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css",
"node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -112,7 +112,7 @@ module.exports = function (grunt) {
],
"pixelegg/css/modern.min.css": [
"api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css",
"node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -134,7 +134,7 @@ module.exports = function (grunt) {
files: {
"jdots/css/high-contrast.min.css": [
"api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css",
"node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -155,7 +155,7 @@ module.exports = function (grunt) {
],
"jdots/css/jdots.min.css": [
"api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css",
"node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css",
@ -175,7 +175,7 @@ module.exports = function (grunt) {
],
"jdots/css/orange-green.min.css": [
"api/js/jquery/chosen/chosen.css",
"vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css",
"node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css",
"vendor/egroupware/magicsuggest/magicsuggest.css",
"api/js/jquery/jquery-ui-timepicker-addon.css",
"api/js/jquery/blueimp/css/blueimp-gallery.min.css",

View File

@ -1,24 +1,29 @@
# EGroupware
| Branch | Status | Tools | Usage |
| ------ | ------ | ----- | ----- |
| master | [![Build Status](https://travis-ci.org/EGroupware/egroupware.svg?branch=master)](https://travis-ci.org/EGroupware/egroupware) | <img src="https://travis-ci.com/images/logos/TravisCI-Full-Color.png" width="108" alt="Travis CI"/> | runs unit-tests after each commit |
| 20.1 | [![Build Status](https://travis-ci.org/EGroupware/egroupware.svg?branch=20.1)](https://travis-ci.org/EGroupware/egroupware) | [![Scrutinizer CI](https://scrutinizer-ci.com/images/logo.png) scrutinizer](https://scrutinizer-ci.com/g/EGroupware/egroupware/) | runs static analysis on our codebase |
| 19.1 | [![Build Status](https://travis-ci.org/EGroupware/egroupware.svg?branch=19.1)](https://travis-ci.org/EGroupware/egroupware) | <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ2scF5HUwLnJVnk2UhYwWpUXHmLQYNXM5yBw&usqp=CAU" width="110" alt="BrowserStack" /> | manual testing with unusual browser versions or platforms |
### Default and prefered installation method for EGroupware is via your package manager:
| Tools | Usage |
| ----- | ----- |
| <img src="https://travis-ci.com/images/logos/TravisCI-Full-Color.png" width="108" alt="Travis CI"/> | runs unit-tests after each commit |
| [![Scrutinizer CI](https://scrutinizer-ci.com/images/logo.png) scrutinizer](https://scrutinizer-ci.com/g/EGroupware/egroupware/) | runs static analysis on our codebase |
| <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQ2scF5HUwLnJVnk2UhYwWpUXHmLQYNXM5yBw&usqp=CAU" width="110" alt="BrowserStack" /> | manual testing with unusual browser versions or platforms |
https://software.opensuse.org/download.html?project=server%3AeGroupWare&package=egroupware-epl
### Default and prefered installation method for EGroupware is via your Linux package manager:
### Installing EGroupware 20.1 via Docker:
EGroupware 20.1 can be installed via Docker, in fact the DEB/RPM packages also does that. Instructions on how to run EGroupware in Docker are in [doc/docker](https://github.com/EGroupware/egroupware/tree/20.1/doc/docker) subdirectory.
* [Installation & Update instructions](https://github.com/EGroupware/egroupware/wiki/Installation-using-egroupware-docker-RPM-DEB-package)
* [Distribution specific instructions](https://github.com/EGroupware/egroupware/wiki/Distribution-specific-instructions)
### Installing EGroupware 19.1 via Docker:
EGroupware 19.1 can be installed via Docker, in fact the DEB/RPM packages also does that. Instructions on how to run EGroupware in Docker are in [doc/docker](https://github.com/EGroupware/egroupware/tree/19.1/doc/docker) subdirectory.
> Every other method (including a developer installation by cloning the repo) is way more complicated AND does not include all features, as part's of EGroupware are running in different containers, eg. the push-server!
### Installing EGroupware 21.1 via Docker for non-Linux environments or not supported Linux distros:
EGroupware 21.1 can be installed via Docker, in fact the DEB/RPM packages also does that. Instructions on how to run EGroupware in Docker are in our [Wiki](https://github.com/EGroupware/egroupware/wiki/Docker-compose-installation) and in [doc/docker](https://github.com/EGroupware/egroupware/tree/21.1/doc/docker) subdirectory.
### Installing EGroupware development version:
### Installing EGroupware development version via Docker:
* this is the prefered developer installation, as it contains eg. a push-server container
* https://github.com/EGroupware/egroupware/tree/master/doc/docker/development
### Deprecated EGroupware development installation:
* install composer.phar from https://getcomposer.org/download/
* optional: for minified JavaScript and CSS install nodejs and grunt
* for JavaScript dependencies and build install nodejs and npm
* optional: for minified CSS install grunt
```
apt/yum/zypper install nodejs
npm install -g grunt-cli

View File

@ -1528,6 +1528,9 @@ class AddressbookApp extends EgwApp
*/
private videoconference_isUserOnline(_action, _selected)
{
// ATM we're not supporting status in mobile theme
if (egwIsMobile()) return false;
let list = app.status ? app.status.getEntireList() : {};
for (let sel in _selected)
{

View File

@ -13,7 +13,6 @@
egw_action_common;
egw_action_popup;
vendor.bower-asset.jquery.dist.jquery;
/vendor/bower-asset/jquery-ui/jquery-ui.js;
*/
import {egwAction,egwActionImplementation} from "./egw_action.js";

View File

@ -12,14 +12,12 @@
/*egw:uses
vendor.bower-asset.jquery.dist.jquery;
egw_menu;
/api/js/jquery/jquery-tap-and-hold/jquery.tapandhold.js;
*/
import {egwAction, egwActionImplementation, egwActionObject} from './egw_action.js';
import {egwFnct} from './egw_action_common.js';
import {egwMenu, _egw_active_menu} from "./egw_menu.js";
import {EGW_KEY_ENTER, EGW_KEY_MENU} from "./egw_action_constants.js";
import "../jquery/jquery-tap-and-hold/jquery.tapandhold.js";
if (typeof window._egwActionClasses == "undefined")
window._egwActionClasses = {};
@ -280,7 +278,39 @@ export function egwPopupActionImplementation()
return false;
};
ai._handleTapHold = function (_node, _callback)
{
let holdTimer = 600;
let maxDistanceAllowed = 40;
let tapTimeout = null;
let startx = 0;
let starty = 0;
//TODO (todo-jquery): ATM we need to convert the possible given jquery dom node object into DOM Element, this
// should be no longer neccessary after removing jQuery nodes.
if (_node instanceof jQuery)
{
_node = _node[0];
}
_node.addEventListener('touchstart', function(e){
tapTimeout = setTimeout(function(event){
_callback(e);
}, holdTimer);
startx = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX;
starty = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY;
});
_node.addEventListener('touchend', function(){
clearTimeout(tapTimeout);
});
_node.addEventListener('touchmove', function(_event){
if (tapTimeout == null) return;
let e = _event.originalEvent;
let x = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX;
let y = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY;
if (Math.sqrt((x-startx)*(x-startx) + (y-starty)+(y-starty)) > maxDistanceAllowed) clearTimeout(tapTimeout);
});
}
/**
* Registers the handler for the context menu
*
@ -292,14 +322,6 @@ export function egwPopupActionImplementation()
ai._registerContext = function(_node, _callback, _context)
{
var contextHandler = function(e) {
if(egwIsMobile())
{
if (e.originalEvent.which == 3)
{
// Enable onhold trigger till we define a better handler for tree contextmenu
// return;
}
}
//Obtain the event object
if (!e)
@ -327,7 +349,7 @@ export function egwPopupActionImplementation()
};
// Safari still needs the taphold to trigger contextmenu
// Chrome has default event on touch and hold which acts like right click
jQuery(_node).bind('taphold', contextHandler);
this._handleTapHold(_node, contextHandler);
jQuery(_node).on('contextmenu', contextHandler);
};

View File

@ -67,6 +67,7 @@ import {et2_template} from "./et2_widget_template";
import {egw} from "../jsapi/egw_global";
import {et2_compileLegacyJS} from "./et2_core_legacyJSFunctions";
import {egwIsMobile} from "../egw_action/egw_action_common.js";
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
//import {et2_selectAccount} from "./et2_widget_SelectAccount";
@ -2078,30 +2079,15 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2
self.selectPopup = null;
};
const $select = jQuery(select.getDOMNode());
$select.find('.ui-multiselect-checkboxes').sortable({
placeholder:'ui-fav-sortable-placeholder',
items:'li[class^="selcolumn_sortable_col"]',
cancel: 'li[class^="selcolumn_sortable_#"]',
cursor: "move",
tolerance: "pointer",
axis: 'y',
containment: "parent",
delay: 250, //(millisecond) delay before the sorting should start
beforeStop: function(event, ui) {
jQuery('li[class^="selcolumn_sortable_#"]', this).css({
opacity: 1
});
},
start: function(event, ui){
jQuery('li[class^="selcolumn_sortable_#"]', this).css({
opacity: 0.5
});
},
sort: function (event, ui)
{
jQuery( this ).sortable("refreshPositions" );
}
let sortablejs = Sortable.create(select.getDOMNode().getElementsByClassName('ui-multiselect-checkboxes')[0], {
ghostClass: 'ui-fav-sortable-placeholder',
draggable: 'li[class^="selcolumn_sortable_col"]',
filter: 'li[class^="selcolumn_sortable_#"]',
direction: 'vertical',
delay: 25,
});
$select.disableSelection();
$select.find('li[class^="selcolumn_sortable_"]').each(function(i,v){
// @ts-ignore

View File

@ -12,7 +12,6 @@
/*egw:uses
/vendor/bower-asset/jquery/dist/jquery.js;
/vendor/bower-asset/jquery-ui/jquery-ui.js;
et2_core_inputWidget;
et2_core_valueWidget;
*/

View File

@ -19,6 +19,7 @@ import {et2_INextmatchHeader} from "./et2_extension_nextmatch";
import {et2_dropdown_button} from "./et2_widget_dropdown_button";
import {ClassWithAttributes} from "./et2_core_inheritance";
import {egw, egw_getFramework} from "../jsapi/egw_global";
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
/**
* Favorites widget, designed for use with a nextmatch widget
@ -177,18 +178,19 @@ export class et2_favorites extends et2_dropdown_button implements et2_INextmatch
}
};
//Add Sortable handler to nm fav. menu
jQuery(this.menu).sortable({
items:'li:not([data-id$="add"])',
placeholder:'ui-fav-sortable-placeholder',
delay: 250, //(millisecond) delay before the sorting should start
update: function ()
{
self.favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'});
self.egw().set_preference(self.options.app,'fav_sort_pref',self.favSortedList);
/**
* todo (@todo-jquery-ui): the sorting does not work at the moment becuase of jquery-ui menu being used in order to create dropdown
* buttons menu. Once we replace the et2_widget_dropdown_button with web component this should be adapted
* and working again.
**/
let sortablejs = Sortable.create(this.menu[0], {
ghostClass: 'ui-fav-sortable-placeholder',
draggable: 'li:not([data-id$="add"])',
delay: 25,
dataIdAttr:'data-id',
onSort: function(event){
self.favSortedList = sortablejs.toArray();
self.egw.set_preference(self.options.app,'fav_sort_pref', self.favSortedList );
sideBoxDOMNodeSort(self.favSortedList);
}
});

View File

@ -23,6 +23,7 @@ import {et2_action_object_impl, et2_DOMWidget} from "./et2_core_DOMWidget";
import {egw_getAppObjectManager, egwActionObject} from '../egw_action/egw_action.js';
import {et2_directChildrenByTagName, et2_filteredNodeIterator, et2_readAttrWithDefault} from "./et2_core_xml";
import {egw} from "../jsapi/egw_global";
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
/**
@ -117,6 +118,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
private wrapper = null;
private lastRowNode: null;
private sortablejs : Sortable = null;
/**
* Constructor
*
@ -943,55 +945,49 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
*/
set_sortable(sortable: boolean | Function)
{
const $node = jQuery(this.getDOMNode());
if(!sortable)
const self = this;
let tbody = this.getDOMNode().getElementsByTagName('tbody')[0];
if(!sortable && this.sortablejs)
{
$node.sortable("destroy");
this.sortablejs.destroy();
return;
}
// Make sure rows have IDs, so sortable has something to return
jQuery('tr', this.tbody).each(function(index) {
const $this = jQuery(this);
for (let i =0; i < tbody.children.length; i++)
{
if (!tbody.children[i].classList.contains('th') && !tbody.children[i].id)
{
tbody.children[i].setAttribute('id', i.toString());
}
}
// Header does not participate in sorting
if($this.hasClass('th')) return;
// If row doesn't have an ID, assign the index as ID
if(!$this.attr("id")) $this.attr("id", index);
});
const self = this;
// Set up sortable
$node.sortable({
// Header does not participate in sorting
items: "> tbody > tr:not(.th)",
distance: 15,
cancel: this.options.sortable_cancel,
placeholder: this.options.sortable_placeholder,
containment: this.options.sortable_containment,
connectWith: this.options.sortable_connectWith,
update: function(event, ui) {
this.sortablejs = new Sortable(tbody,{
group: this.options.sortable_connectWith,
draggable: "tr:not(.th)",
filter: this.options.sortable_cancel,
ghostClass: this.options.sortable_placeholder,
dataIdAttr: 'id',
onAdd:function (event) {
if (typeof self.options.sortable_recieveCallback == 'function') {
self.options.sortable_recieveCallback.call(self, event, this, self.id);
}
},
onStart: function (event, ui) {
if (typeof self.options.sortable_startCallback == 'function') {
self.options.sortable_startCallback.call(self, event, this, self.id);
}
},
onSort: function (event) {
self.egw().json(sortable,[
self.getInstanceManager().etemplate_exec_id,
$node.sortable("toArray"),
self.id],
self.getInstanceManager().etemplate_exec_id,
self.sortablejs.toArray(),
self.id],
null,
self,
true
).sendRequest();
},
receive: function (event, ui) {
if (typeof self.sortable_recieveCallback == 'function') {
self.sortable_recieveCallback.call(self, event, ui, self.id);
}
},
start: function (event, ui) {
if (typeof self.options.sortable_startCallback == 'function') {
self.options.sortable_startCallback.call(self, event, ui, self.id);
}
}
});
}

View File

@ -638,7 +638,7 @@ export class et2_taglist extends et2_selectbox implements et2_IResizeable
});
// if value has already been set, re-set it by it's id(s)
if (this.options.select_options.length && this.options.value.length) {
if (this.options.select_options.length && this.options.value?.length) {
this.set_value(this.options.value.map((v) => v.id));
}
}

View File

@ -25,7 +25,7 @@ import './fw_browser.js';
import './fw_ui.js';
import './fw_classes.js';
import '../jsapi/egw_inheritance.js';
import "sortablejs/Sortable.min.js";
/**
*
* @param {DOMWindow} window
@ -48,31 +48,16 @@ import '../jsapi/egw_inheritance.js';
init: function()
{
this._super.apply(this,arguments);
let self = this;
this.setBottomLine(this.parent.entries);
//Make the base Div sortable. Set all elements with the style "egw_fw_ui_sidemenu_entry_header"
//as handle
if(jQuery(this.elemDiv).data('uiSortable'))
{
jQuery(this.elemDiv).sortable("destroy");
}
jQuery(this.elemDiv).sortable({
handle: ".egw_fw_ui_sidemenu_entry_header",
distance: 15,
start: function(event, ui)
{
var parent = ui.item.context._parent;
parent.isDraged = true;
parent.parent.startDrag.call(parent.parent);
this.elemDiv.classList.add('ui-sortable')
Sortable.create(this.elemDiv,{
onSort: function (evt) {
self.parent.isDraged = true;
self.parent.refreshSort();
},
stop: function(event, ui)
{
var parent = ui.item.context._parent;
parent.parent.stopDrag.call(parent.parent);
parent.parent.refreshSort.call(parent.parent);
},
opacity: 0.7,
axis: 'y'
direction: 'vertical'
});
},
@ -107,31 +92,6 @@ import '../jsapi/egw_inheritance.js';
this._super.apply(this,arguments);
this.sortCallback = _sortCallback;
},
/**
*
* @returns {undefined}
*/
startDrag: function()
{
if (this.activeEntry)
{
jQuery(this.activeEntry.marker).show();
jQuery(this.elemDiv).sortable("refresh");
}
},
/**
*
* @returns {undefined}
*/
stopDrag: function()
{
if (this.activeEntry)
{
jQuery(this.activeEntry.marker).hide();
jQuery(this.elemDiv).sortable("refresh");
}
},
/**
* Called by the sidemenu elements whenever they were sorted. An array containing

View File

@ -1,19 +0,0 @@
jQuery - Tap and Hold
=====================
This jQuery plugin lets you detect a tap and hold event on touch interfaces.
How to use it?
1) Add the jQuery Tap and Hold plugin into your HTML
<script src="jquery.tapandhold.js" type="text/javascript"></script>
2) Bind a tap and hold handler function to the tap and hold event of an element.
$("#myDiv").bind("taphold", function(event){
alert("This is a tap and hold!");
});
You can check a working example in examples/example1.html

View File

@ -1,17 +0,0 @@
<html>
<head>
<title>jQuery - Tap and Hold</title>
<script src="http://code.jquery.com/jquery-1.7.min.js" type="text/javascript"></script>
<script src="https://raw.github.com/zaubersoftware/jquery-tap-and-hold/master/jquery.tapandhold.js" type="text/javascript"></script>
<script type="text/javascript">
$(function(){
$(".tap").bind("taphold", function(){
alert("Hello Tap and Hold World!");
});
});
</script>
</head>
<body>
<div class="tap" style="width:200px; height:200px; background-color: blue; margin: 100px 300px;"></div>
</body>
</html>

View File

@ -1,136 +0,0 @@
/**
* Copyright (c) 2011 Zauber S.A. <http://www.zaubersoftware.com/>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Guido Marucci Blas - guido@zaubersoftware.com
* @description Adds a handler for a custom event 'taphold' that handles a
* tap and hold on touch interfaces.
*/
(function($) {
var TAP_AND_HOLD_TRIGGER_TIMER = 600;
var MAX_DISTANCE_ALLOWED_IN_TAP_AND_HOLD_EVENT = 40;
var TOUCHSTART = "touchstart";
var TOUCHEND = "touchend";
var TOUCHMOVE = "touchmove";
// For debugging only
// var TOUCHSTART = "mousedown";
// var TOUCHEND = "mouseup";
// var TOUCHMOVE = "mousemove";
var tapAndHoldTimer = null;
function calculateEuclideanDistance(x1, y1, x2, y2) {
var diffX = (x2 - x1);
var diffY = (y2 - y1);
return Math.sqrt((diffX * diffX) + (diffY * diffY));
};
function onTouchStart(event) {
var e = event.originalEvent;
// Only start detector if and only if one finger is over the widget
if (!e.touches || (e.targetTouches.length === 1 && e.touches.length === 1)) {
startTapAndHoldDetector.call(this, event)
var element = $(this);
element.bind(TOUCHMOVE, onTouchMove);
element.bind(TOUCHEND, onTouchEnd);
} else {
stopTapAndHoldDetector.call(this);
}
};
function onTouchMove(event) {
if (tapAndHoldTimer == null) {
return;
}
var e = event.originalEvent;
var x = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX;
var y = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY;
var tapAndHoldPoint = $(this).data("taphold.point");
var euclideanDistance = calculateEuclideanDistance(tapAndHoldPoint.x, tapAndHoldPoint.y, x, y);
if (euclideanDistance > MAX_DISTANCE_ALLOWED_IN_TAP_AND_HOLD_EVENT) {
stopTapAndHoldDetector.call(this);
}
};
function onTouchEnd(event) {
stopTapAndHoldDetector.call(this);
};
function onTapAndHold(event) {
clear.call(this);
$(this).data("taphold.handler").call(this, event);
};
function clear() {
tapAndHoldTimer = null;
$(this).unbind(TOUCHMOVE, onTouchMove);
$(this).unbind(TOUCHEND, onTouchEnd);
};
function startTapAndHoldDetector(event) {
if (tapAndHoldTimer != null) {
return;
}
var self = this;
tapAndHoldTimer = setTimeout(function(){
onTapAndHold.call(self, event)
}, TAP_AND_HOLD_TRIGGER_TIMER);
// Stores tap x & y
var e = event.originalEvent;
var tapAndHoldPoint = {};
tapAndHoldPoint.x = (e.changedTouches) ? e.changedTouches[0].pageX: e.pageX;
tapAndHoldPoint.y = (e.changedTouches) ? e.changedTouches[0].pageY: e.pageY;
$(this).data("taphold.point", tapAndHoldPoint);
};
function stopTapAndHoldDetector() {
clearTimeout(tapAndHoldTimer);
clear.call(this);
};
$.event.special["taphold"] = {
setup: function() {
},
add: function(handleObj) {
$(this).data("taphold.handler", handleObj.handler);
if (handleObj.data) {
$(this).bind(TOUCHSTART, handleObj.data, onTouchStart);
} else {
$(this).bind(TOUCHSTART, onTouchStart);
}
},
remove: function(handleObj) {
stopTapAndHoldDetector.call(this);
if (handleObj.data) {
$(this).unbind(TOUCHSTART, handleObj.data, onTouchStart);
} else {
$(this).unbind(TOUCHSTART, onTouchStart);
}
},
teardown: function() {
}
};
})(jQuery);

View File

@ -78,7 +78,7 @@ window.app = {classes: {}};
window.egw_appName = egw_script.getAttribute('data-app');
// split includes in legacy js and modules
const legacy_js_regexp = /\/dhtmlx|jquery-ui/;
const legacy_js_regexp = /\/dhtmlx|jquery-ui-dist/;
// check if egw object was injected by window open
if (typeof window.egw == 'undefined')

View File

@ -17,6 +17,7 @@ import {et2_dialog} from "../etemplate/et2_widget_dialog";
import {et2_createWidget} from "../etemplate/et2_core_widget";
import {et2_favorites} from "../etemplate/et2_widget_favorites";
import type {IegwAppLocal} from "./egw_global";
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
/**
* Type for push-message
@ -781,32 +782,18 @@ export abstract class EgwApp
})
.addClass("ui-helper-clearfix");
//Add Sortable handler to sideBox fav. menu
jQuery('ul','#favorite_sidebox_'+this.appname).sortable({
items:'li:not([data-id$="add"])',
placeholder:'ui-fav-sortable-placeholder',
delay:250, //(millisecond) delay before the sorting should start
helper: function(event, item : any) {
// We'll need to know which app this is for
item.attr('data-appname',self.appname);
// Create custom helper so it can be dragged to Home
var h_parent = item.parent().parent().clone();
h_parent.find('li').not('[data-id="'+item.attr('data-id')+'"]').remove();
h_parent.appendTo('body');
return h_parent;
},
// @ts-ignore
refreshPositions: true,
update: function (event, ui)
{
// @ts-ignore
var favSortedList = jQuery(this).sortable('toArray', {attribute:'data-id'});
self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList);
self._refresh_fav_nm();
}
});
let el = document.getElementById('favorite_sidebox_'+this.appname).getElementsByTagName('ul')[0];
let sortablejs = Sortable.create(el, {
ghostClass: 'ui-fav-sortable-placeholder',
draggable: 'li:not([data-id$="add"])',
delay: 25,
dataIdAttr:'data-id',
onSort: function(event){
let favSortedList = sortablejs.toArray();
self.egw.set_preference(self.appname,'fav_sort_pref',favSortedList);
self._refresh_fav_nm();
}
});
// Bind favorite de-select
var egw_fw = egw_getFramework();

View File

@ -10,7 +10,6 @@
*/
/*egw:uses
/vendor/bower-asset/jquery-ui/jquery-ui.js;
jquery.jquery-ui-timepicker-addon;
egw_core;
@ -19,8 +18,6 @@
egw_css;
*/
import "../../../vendor/bower-asset/jquery/dist/jquery.min.js";
//import "../../../vendor/bower-asset/jquery-ui/jquery-ui.js";
import "../jquery/jquery.noconflict.js";
//import "../jquery/jquery-ui-timepicker-addon.js";
import './egw_core.js';

View File

@ -20,6 +20,13 @@ egw.extend('jsonq', egw.MODULE_GLOBAL, function()
{
"use strict";
/**
* Explicit registered push callbacks
*
* @type {Function[]}
*/
let push_callbacks = [];
/**
* Queued json requests (objects with attributes menuaction, parameters, context, callback, sender and callbeforesend)
*
@ -149,6 +156,34 @@ egw.extend('jsonq', egw.MODULE_GLOBAL, function()
}, 100);
}
return uid;
},
/**
* Register a callback to receive push broadcasts eg. in a popup or iframe
*
* It's also used internally by egw_message's push method to dispatch to the registered callbacks.
*
* @param {Function|PushData} data callback (with bound context) or PushData to dispatch to callbacks
*/
registerPush: function(data)
{
if (typeof data === "function")
{
push_callbacks.push(data);
}
else
{
for (let n in push_callbacks)
{
try {
push_callbacks[n].call(this, data);
}
// if we get an exception, we assume the callback is no longer available and remove it
catch (ex) {
push_callbacks.splice(n, 1);
}
}
}
}
};

View File

@ -459,6 +459,9 @@ egw.extend('message', egw.MODULE_WND_LOCAL, function(_app, _wnd)
app_obj.push(pushData);
}
}
// call the global registered push callbacks
this.registerPush(pushData);
}
};

View File

@ -14,7 +14,7 @@ $setup_info['api']['title'] = 'EGroupware API';
$setup_info['api']['version'] = '21.1';
$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.20210629';
$setup_info['api']['versions']['maintenance_release'] = '21.1.20210723';
$setup_info['api']['enable'] = 3;
$setup_info['api']['app_order'] = 1;
$setup_info['api']['license'] = 'GPL';

View File

@ -425,7 +425,7 @@ class Asyncservice
{
// checking / setting up egw_info/user
//
if ($GLOBALS['egw_info']['user']['account_id'] != $job['account_id'])
//if ($GLOBALS['egw_info']['user']['account_id'] != $job['account_id'])
{
// run notifications, before changing account_id of enviroment
Link::run_notifies();

View File

@ -71,15 +71,18 @@ class Applications
}
/**
* populate array with a list of installed apps
* Populate array with a list of installed apps
*
* egw_applications.app_enabled = -1 is NOT installed, but an uninstalled autoinstall app!
*
* @return array[]
*/
function read_installed_apps()
{
$GLOBALS['egw_info']['apps'] = Api\Cache::getInstance(__CLASS__, 'apps', function()
{
$apps = array();
foreach($this->db->select($this->table_name,'*',false,__LINE__,__FILE__,false,'ORDER BY app_order ASC') as $row)
foreach($this->db->select($this->table_name,'*', ['app_enabled != -1'],__LINE__,__FILE__,false,'ORDER BY app_order ASC') as $row)
{
$apps[$row['app_name']] = Array(
'title' => $row['app_name'],

View File

@ -1003,7 +1003,7 @@ abstract class Framework extends Framework\Extra
self::includeCSS('/api/js/jquery/chosen/chosen.css');
// eTemplate2 uses jQueryUI, so load it first so et2 can override if needed
self::includeCSS("/vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css");
self::includeCSS("/node_modules/jquery-ui-themes/themes/redmond/jquery-ui.css");
// eTemplate2 - load in top so sidebox has styles too
self::includeCSS('/api/templates/default/etemplate2.css');
@ -1074,8 +1074,8 @@ abstract class Framework extends Framework\Extra
));
}
// manually load old legacy javascript dhtmlx & jQuery-UI via script tag
self::includeJS('/vendor/bower-asset/jquery-ui/jquery-ui.js');
self::includeJS('/api/js/jquery/jquery-ui-timepicker-addon.js');
self::includeJS('/node_modules/jquery-ui-dist/jquery-ui.min.js');
self::includeJS('/node_modules/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.js');
self::includeJS('/api/js/dhtmlxtree/codebase/dhtmlxcommon.js');
self::includeJS('/api/js/dhtmlxMenu/sources/dhtmlxmenu.js');
self::includeJS('/api/js/dhtmlxMenu/sources/ext/dhtmlxmenu_ext.js');

View File

@ -241,7 +241,7 @@ class Bundle
// generate api bundle
$inc_mgr->include_js_file('/vendor/bower-asset/jquery/dist/jquery.js');
$inc_mgr->include_js_file('/api/js/jquery/jquery.noconflict.js');
$inc_mgr->include_js_file('/vendor/bower-asset/jquery-ui/jquery-ui.js');
$inc_mgr->include_js_file('/node_modules/jquery-ui-dist/jquery-ui.min.js');
$inc_mgr->include_js_file('/api/js/jsapi/jsapi.js');
$inc_mgr->include_js_file('/api/js/egw_json.js');
$inc_mgr->include_js_file('/api/js/jsapi/egw.js');

View File

@ -2333,14 +2333,19 @@ abstract class Merge
/**
* Merge the selected IDs into the given document, save it to the VFS, then
* either open it in the editor or have the browser download the file.
*
* @param String[]|null $ids Allows extending classes to process IDs in their own way. Leave null to pull from request.
* @param Merge|null $document_merge Already instantiated Merge object to do the merge.
* @throws Api\Exception
* @throws Api\Exception\AssertionFailed
*/
public static function merge_entries()
public static function merge_entries(array $ids = null, Merge &$document_merge = null)
{
if (class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge'))
if (is_null($document_merge) && class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge'))
{
$document_merge = new $_REQUEST['merge']();
}
else
elseif (is_null($document_merge))
{
$document_merge = new Api\Contacts\Merge();
}
@ -2351,13 +2356,16 @@ abstract class Merge
return;
}
$ids = is_string($_REQUEST['id']) && strpos($_REQUEST['id'],'[') === FALSE ? explode(',',$_REQUEST['id']) : json_decode($_REQUEST['id'],true);
if(is_null(($ids)))
{
$ids = is_string($_REQUEST['id']) && strpos($_REQUEST['id'], '[') === FALSE ? explode(',', $_REQUEST['id']) : json_decode($_REQUEST['id'], true);
}
if($_REQUEST['select_all'] === 'true')
{
$ids = self::get_all_ids($document_merge);
}
$filename = '';
$filename = $document_merge->get_filename($_REQUEST['document']);
$result = $document_merge->merge_file($_REQUEST['document'], $ids, $filename, '', $header);
if(!is_file($result) || !is_readable($result))
@ -2412,6 +2420,17 @@ abstract class Merge
}
}
/**
* Generate a filename for the merged file
*
* Default is just the name of the template
* @return string
*/
protected function get_filename($document) : string
{
return '';
}
/**
* Get all ids for when they try to do 'Select All', then merge into document
*

View File

@ -82,7 +82,10 @@ class Vfs extends Vfs\Base
*/
static $is_root = false;
/**
* Current user id, in case we ever change if away from $GLOBALS['egw_info']['user']['account_id']
* Current Vfs user id, set from $GLOBALS['egw_info']['user']['account_id'] by self::init_static()
*
* Should be protected and moved to Vfs\Base plus a getter and setter method added for public access,
* as after setting it in 21.1+, Api\Vfs\StreamWrapper::init_static() need to be called to set the default user context!
*
* @var int
*/
@ -749,22 +752,22 @@ class Vfs extends Vfs\Base
* @return boolean
* @todo deprecated or even remove $user parameter and code
*/
static function check_access($path, $check, $stat=null, $user=null)
static function check_access($path, $check, $stat=null, int $user=null)
{
static $vfs = null;
if (is_null($stat) && $user && $user != self::$user)
if (is_null($stat) && $user && $user !== self::$user)
{
static $path_user_stat = array();
$backup_user = self::$user;
self::$user = $user;
Vfs\StreamWrapper::init_static();
self::clearstatcache($path);
if (!isset($path_user_stat[$path]) || !isset($path_user_stat[$path][$user]))
{
self::clearstatcache($path);
if (!isset($vfs)) $vfs = new Vfs\StreamWrapper();
$vfs = new Vfs\StreamWrapper();
$path_user_stat[$path][$user] = $vfs->url_stat($path, 0);
self::clearstatcache($path); // we need to clear the stat-cache after the call too, as the next call might be the regular user again!
@ -786,6 +789,8 @@ class Vfs extends Vfs\Base
$ret = false; // no access, if we can not stat the file
}
self::$user = $backup_user;
Vfs\StreamWrapper::init_static();
$vfs = null;
// we need to clear stat-cache again, after restoring original user, as eg. eACL is stored in session
self::clearstatcache($path);

View File

@ -254,10 +254,11 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
{
$umaskbefore = umask();
if (self::LOG_LEVEL > 1) error_log(__METHOD__." about to call mkdir for $fs_dir # Present UMASK:".decoct($umaskbefore)." called from:".function_backtrace());
self::mkdir_recursive($fs_dir,0700,true);
// if running as root eg. via (docker exec) filemanager/cli.php do NOT create dirs not readable by webserver
self::mkdir_recursive($fs_dir,function_exists('posix_getuid') && !posix_getuid() ? 0777 : 0700,true);
}
}
// check if opend file is a directory
// check if opened file is a directory
elseif($stat && ($stat['mode'] & self::MODE_DIR) == self::MODE_DIR)
{
if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) Is a directory!");
@ -308,6 +309,11 @@ class StreamWrapper extends Api\Db\Pdo implements Vfs\StreamWrapperIface
if ($this->operation == self::STORE2FS)
{
if (self::LOG_LEVEL > 1) error_log(__METHOD__." fopen (may create a directory? mkdir) ($this->opened_fs_id,$mode,$options)");
// if creating a new file as root eg. via (docker exec) filemanager/cli.php do NOT create files unreadable by webserver
if ($new_file && function_exists('posix_getuid') && !posix_getuid())
{
umask(0666);
}
if (!($this->opened_stream = fopen(self::_fs_path($this->opened_fs_id),$mode)) && $new_file)
{
// delete db entry again, if we are not able to open a new(!) file

View File

@ -972,6 +972,7 @@ class StreamWrapper extends Base implements StreamWrapperIface
/**
* Init our static properties and register this wrapper
*
* Must be called when Vfs::$user is changed!
*/
static function init_static()
{
@ -984,16 +985,31 @@ class StreamWrapper extends Base implements StreamWrapperIface
{
self::$fstab = $fstab;
}
if (!empty($GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab']) &&
is_array($GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab']))
// get the user Vfs is currently using, might be different from $GLOBALS['egw_info']['user']['account_id']
if (!isset(Vfs::$user))
{
self::$fstab += $GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab'];
Vfs::init_static();
}
if (Vfs::$user != $GLOBALS['egw_info']['user']['account_id'])
{
$prefs = new Api\Preferences(Vfs::$user);
$vfs_fstab = $prefs->data['common']['vfs_fstab'];
}
else
{
$vfs_fstab = $GLOBALS['egw_info']['user']['preferences']['common']['vfs_fstab'];
}
if (!empty($vfs_fstab) && is_array($vfs_fstab))
{
self::$fstab += $vfs_fstab;
}
// set default context for our schema ('vfs') with current user
if (!($context = stream_context_get_options(stream_context_get_default())) || empty($context[self::SCHEME]['user']))
if (!($context = stream_context_get_options(stream_context_get_default())) || empty($context[self::SCHEME]['user']) ||
$context[self::SCHEME]['user'] !== (int)Vfs::$user)
{
$context[self::SCHEME]['user'] = (int)$GLOBALS['egw_info']['user']['account_id'];
$context[self::SCHEME]['user'] = (int)Vfs::$user;
stream_context_set_default($context);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -85,7 +85,7 @@
vertical-align: top;
display: inline-block;
background-color: transparent;
background-image: url(../../../vendor/bower-asset/jquery-ui/themes/redmond/images/ui-icons_469bdd_256x240.png);
background-image: url(../../../node_modules/jquery-ui-themes/themes/redmond/images/ui-icons_469bdd_256x240.png);
border: none;
box-shadow: none;
}

View File

@ -107,7 +107,7 @@
vertical-align: top;
display: inline-block;
background-color: transparent;
background-image: url(../../../vendor/bower-asset/jquery-ui/themes/redmond/images/ui-icons_469bdd_256x240.png);
background-image: url(../../../node_modules/jquery-ui-themes/themes/redmond/images/ui-icons_469bdd_256x240.png);
border: none;
box-shadow: none;
}

View File

@ -95,7 +95,7 @@
vertical-align: top;
display: inline-block;
background-color: transparent;
background-image: url(../../../vendor/bower-asset/jquery-ui/themes/redmond/images/ui-icons_469bdd_256x240.png);
background-image: url(../../../node_modules/jquery-ui-themes/themes/redmond/images/ui-icons_469bdd_256x240.png);
border: none;
box-shadow: none;
}

View File

@ -38,9 +38,13 @@
"issuses": "https://my.egroupware.org"
},
"repositories": [
{
"type": "pear",
"url": "https://pear.horde.org"
{
"type": "composer",
"url": "https://asset-packagist.org"
},
{
"type": "pear",
"url": "https://pear.horde.org"
},
{
"type": "vcs",
@ -51,6 +55,9 @@
"platform": {
"php": "7.3"
},
"fxp-asset": {
"enabled": false
},
"sort-packages": true
},
"require": {
@ -70,14 +77,12 @@
"bower-asset/fastclick": "1.0.*",
"bower-asset/jquery": "^1.12.4",
"bower-asset/jquery-touchswipe": "1.6.*",
"bower-asset/jquery-ui": "=1.11.2",
"egroupware/activesync": "self.version",
"egroupware/adodb-php": "self.version",
"egroupware/bookmarks": "self.version",
"egroupware/collabora": "self.version",
"egroupware/guzzlestream": "dev-master",
"egroupware/icalendar": "^2.1.9",
"egroupware/imap-client": "^2.30.2",
"egroupware/magicsuggest": "^2.1",
"egroupware/news_admin": "self.version",
"egroupware/openid": "self.version",
@ -90,13 +95,13 @@
"egroupware/tracker": "self.version",
"egroupware/webdav": "dev-master",
"egroupware/z-push-dev": "^2.5",
"fxp/composer-asset-plugin": "^1.2.2",
"giggsey/libphonenumber-for-php": "^8.12",
"npm-asset/as-jqplot": "1.0.*",
"npm-asset/gridster": "0.5.*",
"oomphinc/composer-installers-extender": "^1.1",
"pear-pear.horde.org/horde_compress": "^2.0.8",
"pear-pear.horde.org/horde_crypt": "^2.7.9",
"pear-pear.horde.org/horde_imap_client": "^2.30.3",
"pear-pear.horde.org/horde_mail": "^2.1.2",
"pear-pear.horde.org/horde_managesieve": "^1.0.2",
"pear-pear.horde.org/horde_mapi": "^1.0.9",

910
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -64,7 +64,7 @@ else \
RESULT=$?; \
fi; \
rm composer-setup.php; \
composer.phar self-update 1.8.6; \
composer.phar self-update 1.10.22; \
exit $RESULT' \
# disable certificate checks for LDAP as most LDAP and AD servers have no "valid" cert
&& echo "TLS_REQCERT never" >> /etc/ldap/ldap.conf

View File

@ -69,7 +69,7 @@ fi; \
rm composer-setup.php; \
exit $RESULT' \
# build EGroupware
&& composer.phar self-update 1.8.6 \
&& composer.phar self-update 1.10.22 \
&& cd /usr/share \
&& [ $PHP_VERSION = "8.0" ] && COMPOSER_EXTRA=--ignore-platform-reqs || true \
&& composer.phar create-project $COMPOSER_EXTRA --prefer-dist --no-scripts --no-dev egroupware/egroupware:$VERSION \

36
doc/ldif2sql.php Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env php
<?php
if (!$_SERVER['argc'])
{
echo "cat test.ldif | ".basename($_SERVER['argv'][0])."attr1[, attr2[, ...]]\n";
exit(1);
}
$attrs = array_slice($_SERVER['argv'], 1);
$values = $rows = [];
while(!feof(STDIN))
{
$line = trim(fgets(STDIN));
if (empty($line) || $line[0] === '#' ||
!preg_match('/^([^:]+): (.*)$/', $line, $matches))
{
$values = [];
continue;
}
if ($matches[1] === 'dn') $values = [];
$values[$matches[1]] = $matches[2];
if (count(array_intersect(array_keys($values), $attrs)) === count($attrs))
{
$cols = [];
foreach($attrs as $attr)
{
$cols[$attr] = "'".addslashes($values[$attr])."'";
}
$cols = '('.implode(', ', $cols).')';
if (!in_array($cols, $rows)) $rows[] = $cols;
}
}
echo implode(",\n", $rows)."\n";

View File

@ -1,3 +1,14 @@
egroupware-docker (21.1.20210723) hardy; urgency=low
* Security Update: all 21.1 users should upgrade ASAP, 20.1 and below is not affected
* Filemanager/VFS: when creating a new file as root eg. via (docker exec) filemanager/cli.php do NOT create files unreadable by webserver
* Collabora: Fix editing files in mounted share
* Kanban/PostgreSQL: fix installation of example board under PostgreSQL
* smallPART/PostgreSQL: fix multiple SQL errors
* smallPART/PostgreSQL: fix installation under PostgreSQL
-- Ralf Becker <rb@egroupware.org> Fri, 23 Jul 2021 08:09:49 +0200
egroupware-docker (21.1.20210629) hardy; urgency=low
* Mail/Admin: fix not working mail wizard

View File

@ -75,8 +75,8 @@
</row>
<row class="$row_cont[info_cat] $row_cont[class]" valign="top">
<hbox align="center" class="infolog_CompletedClmn">
<image label="$row_cont[info_type]" src="${row}[info_type]" default_src="infolog/navbar"/>
<image label="$row_cont[info_status_label]" id="edit_status[$row_cont[info_id]]" href="javascript:egw.open($row_cont[info_id],'infolog');" src="$row_cont[info_status_label]" default_src="status"/>
<image label="$row_cont[info_type]" src="infolog/${row}[info_type]" default_src="infolog/navbar"/>
<image label="$row_cont[info_status_label]" id="edit_status[$row_cont[info_id]]" href="javascript:egw.open($row_cont[info_id],'infolog');" src="infolog/$row_cont[info_status_label]" default_src="status"/>
<image label="$row_cont[info_percent]" id="edit_percent[$row_cont[info_id]]" href="javascript:egw.open($row_cont[info_id],'infolog');" src="$row_cont[info_percent]"/>
<progress label="$row_cont[info_percent]" id="{$row}[info_percent2]" href="javascript:egw.open($row_cont[info_id],'infolog');"/>
</hbox>

View File

@ -176,6 +176,24 @@ class mail_hooks
'always_display' => lang('always show html emails'),
);
$contactLabelOptions = array (
'n_prefix' => array (
'id' => 'n_prefix',
'label' => lang('Prefix'),
),
'n_given' => array (
'id' => 'n_given',
'label' => lang('First name')
),
'n_family' => array(
'id' => 'n_family',
'label' => lang('Last name')
),
'org_name' => array(
'id' => 'org_name',
'label' => lang('Organisation')
)
);
// otherwise we get warnings during setup
if (!is_array($folderList)) $folderList = array();
@ -457,29 +475,12 @@ class mail_hooks
'label' => 'Contact label',
'help' => 'Defines what to show as contact label for added contact into To/Cc/Bcc when composing an email. Default is firstname lastname and empty means include eveything available.',
'name' => 'contactLabel',
'values' => '',
'values' => $contactLabelOptions,
'attributes' => array(
'allowFreeEntries' => false,
'editModeEnabled' => false,
'autocomplete_url' => ' ',
'select_options' => array (
'n_prefix' => array (
'id' => 'n_prefix',
'label' => lang('Prefix'),
),
'n_given' => array (
'id' => 'n_given',
'label' => lang('First name')
),
'n_family' => array(
'id' => 'n_family',
'label' => lang('Last name')
),
'org_name' => array(
'id' => 'org_name',
'label' => lang('Organisation')
)
)
'select_options' => $contactLabelOptions
),
'default' => ['n_given','n_family']
)

2335
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -45,13 +45,13 @@
}
},
"dependencies": {
"@lion/button": "^0.14.1",
"@lion/core": "^0.18.1",
"@lion/input": "^0.15.3",
"carbon-components": "^10.37.0",
"carbon-web-components": "^1.14.1",
"@andxor/jquery-ui-touch-punch-fix": "^1.0.2",
"jquery-ui-dist": "^1.12.1",
"jquery-ui-themes": "^1.12.0",
"jquery-ui-timepicker-addon": "^1.6.3",
"lit-element": "^2.5.1",
"lit-html": "^1.4.1"
"lit-html": "^1.4.1",
"sortablejs": "^1.14.0"
},
"engines": {
"node": ">=14.0.0"

View File

@ -23,7 +23,7 @@ import '../../api/js/framework/fw_browser.js';
import '../../api/js/framework/fw_ui.js';
import '../../api/js/framework/fw_classes.js';
import '../../api/js/jsapi/egw_inheritance.js';
import '@andxor/jquery-ui-touch-punch-fix/jquery.ui.touch-punch.js';
/**
*
* @param {DOMWindow} window

View File

@ -226,6 +226,9 @@ if (!empty($detail = $_GET['detail']))
{
switch($key)
{
case 'autoinstall':
$val = json_encode($val);
break;
case 'title':
continue 2;
case 'tables':

View File

@ -516,16 +516,17 @@ class setup
),False,__LINE__,__FILE__);
}
try {
$this->db->insert($this->applications_table,array(
'app_name' => $appname,
'app_enabled' => $enable,
'app_order' => $setup_info[$appname]['app_order'],
'app_tables' => (string)$tables, // app_tables is NOT NULL
'app_version' => $setup_info[$appname]['version'],
'app_index' => $setup_info[$appname]['index'],
'app_icon' => $setup_info[$appname]['icon'],
'app_icon_app' => $setup_info[$appname]['icon_app'],
),False,__LINE__,__FILE__);
$this->db->insert($this->applications_table, [
'app_enabled' => $enable,
'app_order' => $setup_info[$appname]['app_order'],
'app_tables' => (string)$tables, // app_tables is NOT NULL
'app_version' => $setup_info[$appname]['version'],
'app_index' => $setup_info[$appname]['index'],
'app_icon' => $setup_info[$appname]['icon'],
'app_icon_app' => $setup_info[$appname]['icon_app'],
], [
'app_name' => $appname,
], __LINE__, __FILE__);
}
catch (Api\Db\Exception\InvalidSql $e)
{
@ -548,7 +549,7 @@ class setup
* Check if an application has info in the db
*
* @param $appname Application 'name' with a matching $setup_info[$appname] array slice
* @param $enabled optional, set to False to not enable this app
* @return boolean|null null: autoinstalled app which got uninstalled
*/
function app_registered($appname)
{
@ -563,13 +564,13 @@ class setup
// _debug_array($setup_info[$appname]);
}
if ($this->db->select($this->applications_table,'COUNT(*)',array('app_name' => $appname),__LINE__,__FILE__)->fetchColumn())
if (($enabled = $this->db->select($this->applications_table, 'app_enabled', ['app_name' => $appname], __LINE__,__FILE__)->fetchColumn()) !== false)
{
if(@$GLOBALS['DEBUG'])
{
echo '... app previously registered.';
}
return True;
return $enabled <= -1 ? null : true;
}
if(@$GLOBALS['DEBUG'])
{
@ -676,10 +677,35 @@ class setup
$this->db->delete(Api\Config::TABLE, array('config_app'=>$appname),__LINE__,__FILE__);
}
//echo 'DELETING application: ' . $appname;
$this->db->delete($this->applications_table,array('app_name'=>$appname),__LINE__,__FILE__);
// when uninstalling an autoinstall app, we must mark it deleted in the DB, otherwise it will install again the next update
if (file_exists($file = EGW_SERVER_ROOT.'/'.$appname.'/setup/setup.inc.php'))
{
$setup_info = [];
include($file);
}
if (!empty($setup_info[$appname]['autoinstall']) && $setup_info[$appname]['autoinstall'] === true)
{
$this->db->update($this->applications_table, [
'app_enabled' => -1,
'app_tables' => '',
'app_version' => 'uninstalled',
'app_index' => null,
], [
'app_name' => $appname,
], __LINE__, __FILE__);
}
else
{
$this->db->delete($this->applications_table, ['app_name' => $appname], __LINE__, __FILE__);
}
Api\Egw\Applications::invalidate();
// unregister hooks, before removing links
unset($GLOBALS['egw_info']['apps'][$appname]);
Api\Hooks::read(true);
// Remove links to the app
Link::unlink(0, $appname);
}
@ -1219,6 +1245,8 @@ class setup
{
static $table_names = False;
if(!is_object($this->db)) $this->loaddb();
if (!$table_names || $force_refresh) $table_names = $this->db->table_names();
if (!$table_names) return false;

View File

@ -62,7 +62,7 @@ class setup_detection
/* one of these tables exists. checking for post/pre beta version */
if($GLOBALS['egw_setup']->applications_table != 'applications')
{
foreach($GLOBALS['egw_setup']->db->select($GLOBALS['egw_setup']->applications_table,'*',false,__LINE__,__FILE__) as $row)
foreach($GLOBALS['egw_setup']->db->select($GLOBALS['egw_setup']->applications_table, '*', 'app_enabled != -1', __LINE__, __FILE__) as $row)
{
$app = $row['app_name'];
if (!isset($setup_info[$app])) // app source no longer there

View File

@ -618,10 +618,10 @@ class setup_process
foreach($setup_info as $appname => &$appdata)
{
// check if app is NOT installed
if(!$GLOBALS['egw_setup']->app_registered($appname))
if (!($registered = $GLOBALS['egw_setup']->app_registered($appname)))
{
// check if app wants to be automatically installed on update to version x or allways
if (isset($appdata['autoinstall']) && ($appdata['autoinstall'] === true ||
// check if app wants to be automatically installed on update to version x or always (unless uninstalled prior)
if (isset($appdata['autoinstall']) && ($appdata['autoinstall'] === true && $registered !== null ||
$appdata['autoinstall'] === $this->api_version_target))
{
$info_c = $this->current(array($appname => $appdata), $DEBUG);