mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 06:30:59 +01:00
Merge branch 'master' into web-components
This commit is contained in:
commit
fac1dfb8d4
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||
|
16
Gruntfile.js
16
Gruntfile.js
@ -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",
|
||||
|
31
README.md
31
README.md
@ -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
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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);
|
@ -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')
|
||||
|
@ -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();
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
|
@ -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'],
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
910
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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
36
doc/ldif2sql.php
Executable 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";
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
2335
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -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"
|
||||
|
@ -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
|
||||
|
@ -226,6 +226,9 @@ if (!empty($detail = $_GET['detail']))
|
||||
{
|
||||
switch($key)
|
||||
{
|
||||
case 'autoinstall':
|
||||
$val = json_encode($val);
|
||||
break;
|
||||
case 'title':
|
||||
continue 2;
|
||||
case 'tables':
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user