Merge branch 'master' into new-js-loader to bring it up to date

This commit is contained in:
nathangray 2021-06-23 15:40:50 -06:00
commit 6559a053f3
20 changed files with 284 additions and 40 deletions

View File

@ -14,7 +14,7 @@
import { EgwApp } from '../../api/js/jsapi/egw_app'; import { EgwApp } from '../../api/js/jsapi/egw_app';
import { etemplate2 } from "../../api/js/etemplate/etemplate2"; import { etemplate2 } from "../../api/js/etemplate/etemplate2";
import { et2_dialog } from "../../api/js/etemplate/et2_widget_dialog"; import { et2_dialog } from "../../api/js/etemplate/et2_widget_dialog";
import { fetchAll } from "../../api/js/etemplate/et2_extension_nextmatch_actions.js"; import { nm_action, fetchAll } from "../../api/js/etemplate/et2_extension_nextmatch_actions.js";
import "./CRM.js"; import "./CRM.js";
import { egw } from "../../api/js/jsapi/egw_global"; import { egw } from "../../api/js/jsapi/egw_global";
/** /**

View File

@ -17,7 +17,7 @@ import {EgwApp, PushData} from '../../api/js/jsapi/egw_app';
import {etemplate2} from "../../api/js/etemplate/etemplate2"; import {etemplate2} from "../../api/js/etemplate/etemplate2";
import {et2_dialog} from "../../api/js/etemplate/et2_widget_dialog"; import {et2_dialog} from "../../api/js/etemplate/et2_widget_dialog";
import {et2_selectbox} from "../../api/js/etemplate/et2_widget_selectbox"; import {et2_selectbox} from "../../api/js/etemplate/et2_widget_selectbox";
import {fetchAll} from "../../api/js/etemplate/et2_extension_nextmatch_actions.js"; import {nm_action, fetchAll} from "../../api/js/etemplate/et2_extension_nextmatch_actions.js";
import "./CRM.js"; import "./CRM.js";
import {egw} from "../../api/js/jsapi/egw_global"; import {egw} from "../../api/js/jsapi/egw_global";

View File

@ -148,7 +148,7 @@ export class et2_DOMWidget extends et2_widget {
*/ */
insertChild(_node, _idx) { insertChild(_node, _idx) {
super.insertChild(_node, _idx); super.insertChild(_node, _idx);
if (_node.instanceOf(et2_DOMWidget) && typeof _node.hasOwnProperty('parentNode') && this.getDOMNode(this)) { if (_node.instanceOf && _node.instanceOf(et2_DOMWidget) && typeof _node.hasOwnProperty('parentNode') && this.getDOMNode(this)) {
try { try {
_node.setParentDOMNode(this.getDOMNode(_node)); _node.setParentDOMNode(this.getDOMNode(_node));
} }
@ -157,6 +157,10 @@ export class et2_DOMWidget extends et2_widget {
// will probably try again in doLoadingFinished() // will probably try again in doLoadingFinished()
} }
} }
// _node is actually a Web Component
else if (_node instanceof Element) {
this.getDOMNode().append(_node);
}
} }
isAttached() { isAttached() {
return this.parentNode != null; return this.parentNode != null;

View File

@ -270,7 +270,7 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
{ {
super.insertChild(_node, _idx); super.insertChild(_node, _idx);
if(_node.instanceOf(et2_DOMWidget) && typeof _node.hasOwnProperty('parentNode') && this.getDOMNode(this)) if(_node.instanceOf && _node.instanceOf(et2_DOMWidget) && typeof _node.hasOwnProperty('parentNode') && this.getDOMNode(this))
{ {
try try
{ {
@ -282,6 +282,11 @@ export abstract class et2_DOMWidget extends et2_widget implements et2_IDOMNode
// will probably try again in doLoadingFinished() // will probably try again in doLoadingFinished()
} }
} }
// _node is actually a Web Component
else if (_node instanceof Element )
{
this.getDOMNode().append(_node);
}
} }
isAttached() { isAttached() {

View File

@ -544,11 +544,32 @@ export class et2_widget extends ClassWithAttributes {
this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype); this.parseXMLAttrs(_node.attributes, attributes, constructor.prototype);
// Do an sanity check for the attributes // Do an sanity check for the attributes
ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes); ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes);
// Creates the new widget, passes this widget as an instance and if (undefined == window.customElements.get(_nodeName)) {
// passes the widgetType. Then it goes on loading the XML for it. // Creates the new widget, passes this widget as an instance and
var widget = new constructor(this, attributes); // passes the widgetType. Then it goes on loading the XML for it.
// Load the widget itself from XML var widget = new constructor(this, attributes);
widget.loadFromXML(_node); // Load the widget itself from XML
widget.loadFromXML(_node);
}
else {
widget = this.loadWebComponent(_nodeName, _node, attributes);
if (this.addChild) {
// webcomponent going into old et2_widget
this.addChild(widget);
}
}
return widget;
}
/**
* Load a Web Component
* @param _nodeName
* @param _node
*/
loadWebComponent(_nodeName, _node, attributes) {
let widget = document.createElement(_nodeName);
widget.textContent = _node.textContent;
// Apply any set attributes
_node.getAttributeNames().forEach(attribute => widget.setAttribute(attribute, attributes[attribute]));
return widget; return widget;
} }
/** /**

View File

@ -25,6 +25,7 @@ import {et2_IDOMNode, et2_IInputNode} from "./et2_core_interfaces";
// fixing circular dependencies by only importing type // fixing circular dependencies by only importing type
import type {et2_container} from "./et2_core_baseWidget"; import type {et2_container} from "./et2_core_baseWidget";
import type {et2_inputWidget, et2_input} from "./et2_core_inputWidget"; import type {et2_inputWidget, et2_input} from "./et2_core_inputWidget";
import {decorateLanguageService} from "ts-lit-plugin/lib/decorate-language-service";
/** /**
* The registry contains all XML tag names and the corresponding widget * The registry contains all XML tag names and the corresponding widget
@ -716,12 +717,40 @@ export class et2_widget extends ClassWithAttributes
// Do an sanity check for the attributes // Do an sanity check for the attributes
ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes); ClassWithAttributes.generateAttributeSet(et2_attribute_registry[constructor.name], attributes);
// Creates the new widget, passes this widget as an instance and if(undefined == window.customElements.get(_nodeName))
// passes the widgetType. Then it goes on loading the XML for it. {
var widget = new constructor(this, attributes); // Creates the new widget, passes this widget as an instance and
// passes the widgetType. Then it goes on loading the XML for it.
var widget = new constructor(this, attributes);
// Load the widget itself from XML // Load the widget itself from XML
widget.loadFromXML(_node); widget.loadFromXML(_node);
}
else
{
widget = this.loadWebComponent(_nodeName, _node, attributes);
if(this.addChild)
{
// webcomponent going into old et2_widget
this.addChild(widget);
}
}
return widget;
}
/**
* Load a Web Component
* @param _nodeName
* @param _node
*/
loadWebComponent(_nodeName : string, _node, attributes : Object) : HTMLElement
{
let widget = document.createElement(_nodeName);
widget.textContent = _node.textContent;
// Apply any set attributes
_node.getAttributeNames().forEach(attribute => widget.setAttribute(attribute, attributes[attribute]));
return widget; return widget;
} }

View File

@ -45,6 +45,11 @@ export function nm_action(_action, _senders, _target, _ids)
_ids = nm.getSelection(); _ids = nm.getSelection();
_action.data.nextmatch = nm; _action.data.nextmatch = nm;
} }
else
{
// This will probably fail without nm, but it depends on the action
_ids = {ids: _senders.map(function(s) {return s.id;})};
}
} }
// row ids // row ids
var row_ids = ""; var row_ids = "";

View File

@ -31,6 +31,7 @@ class Merge extends Api\Storage\Merge
var $public_functions = array( var $public_functions = array(
'download_by_request' => true, 'download_by_request' => true,
'show_replacements' => true, 'show_replacements' => true,
"merge_entries" => true
); );
/** /**

View File

@ -1085,7 +1085,8 @@ abstract class Framework extends Framework\Extra
$java_script .= '<script type="importmap" nonce="'.htmlspecialchars(ContentSecurityPolicy::addNonce('script-src')).'">'."\n". $java_script .= '<script type="importmap" nonce="'.htmlspecialchars(ContentSecurityPolicy::addNonce('script-src')).'">'."\n".
json_encode(self::getImportMap(), JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)."\n". json_encode(self::getImportMap(), JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)."\n".
"</script>\n"; "</script>\n";
$java_script .='<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.44/dist/themes/base.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.44/dist/shoelace.js"></script>';
// load our clientside entrypoint egw.js // load our clientside entrypoint egw.js
$java_script .= '<script type="module" src="'.$GLOBALS['egw_info']['server']['webserver_url']. $java_script .= '<script type="module" src="'.$GLOBALS['egw_info']['server']['webserver_url'].
'/api/js/jsapi/egw.js?'.filemtime(EGW_SERVER_ROOT.'/api/js/jsapi/egw.js').'" id="egw_script_id"'; '/api/js/jsapi/egw.js?'.filemtime(EGW_SERVER_ROOT.'/api/js/jsapi/egw.js').'" id="egw_script_id"';
@ -1151,6 +1152,9 @@ abstract class Framework extends Framework\Extra
$prefix.'/api/js/jquery/jquery.noconflict.js'; $prefix.'/api/js/jquery/jquery.noconflict.js';
}, $imports); }, $imports);
ContentSecurityPolicy::add("script-src","https://cdn.skypack.dev");
ContentSecurityPolicy::add("script-src","https://cdn.jsdelivr.net");
ContentSecurityPolicy::add("style-src","https://cdn.jsdelivr.net");
return ['imports' => $imports]; return ['imports' => $imports];
} }

View File

@ -15,6 +15,7 @@ namespace EGroupware\Api\Storage;
use DOMDocument; use DOMDocument;
use EGroupware\Api; use EGroupware\Api;
use EGroupware\Api\Vfs;
use EGroupware\Stylite; use EGroupware\Stylite;
use tidy; use tidy;
use uiaccountsel; use uiaccountsel;
@ -77,6 +78,9 @@ abstract class Merge
public $export_limit; public $export_limit;
public $public_functions = array(
"merge_entries" => true
);
/** /**
* Configuration for HTML Tidy to clean up any HTML content that is kept * Configuration for HTML Tidy to clean up any HTML content that is kept
*/ */
@ -2083,10 +2087,6 @@ abstract class Merge
{ {
self::document_mail_action($documents['document'], $file); self::document_mail_action($documents['document'], $file);
} }
else if ($editable_mimes[$file['mime']])
{
self::document_editable_action($documents['document'], $file);
}
} }
$files = array(); $files = array();
@ -2164,10 +2164,6 @@ abstract class Merge
{ {
self::document_mail_action($current_level[$prefix.$file['name']], $file); self::document_mail_action($current_level[$prefix.$file['name']], $file);
} }
else if ($editable_mimes[$file['mime']])
{
self::document_editable_action($current_level[$prefix.$file['name']], $file);
}
break; break;
default: default:
@ -2198,16 +2194,21 @@ abstract class Merge
} }
$documents[$file['mime']]['children'][$prefix.$file['name']] = array( $documents[$file['mime']]['children'][$prefix.$file['name']] = array(
'caption' => Api\Vfs::decodePath($file['name']), 'caption' => Api\Vfs::decodePath($file['name']),
'target' => '_blank',
'postSubmit' => true, // download needs post submit (not Ajax) to work 'postSubmit' => true, // download needs post submit (not Ajax) to work
); );
$edit_attributes = array(
'menuaction' => $GLOBALS['egw_info']['flags']['currentapp'].'.'.get_called_class().'.merge_entries',
'document' => $file['path'],
'merge' => get_called_class(),
'id' => '$id',
'select_all' => '$select_all'
);
$documents[$file['mime']]['children'][$prefix.$file['name']]['url'] = urldecode(http_build_query($edit_attributes));
if ($file['mime'] == 'message/rfc822') if ($file['mime'] == 'message/rfc822')
{ {
self::document_mail_action($documents[$file['mime']]['children'][$prefix.$file['name']], $file); self::document_mail_action($documents[$file['mime']]['children'][$prefix.$file['name']], $file);
} }
else if ($editable_mimes[$file['mime']])
{
self::document_editable_action($documents[$file['mime']]['children'][$prefix.$file['name']], $file);
}
} }
else else
{ {
@ -2215,16 +2216,21 @@ abstract class Merge
'icon' => Api\Vfs::mime_icon($file['mime']), 'icon' => Api\Vfs::mime_icon($file['mime']),
'caption' => Api\Vfs::decodePath($file['name']), 'caption' => Api\Vfs::decodePath($file['name']),
'group' => 2, 'group' => 2,
'postSubmit' => true, // download needs post submit (not Ajax) to work 'target' => '_blank'
); );
$edit_attributes = array(
'menuaction' => $GLOBALS['egw_info']['flags']['currentapp'].'.'.get_called_class().'.merge_entries',
'document' => $file['path'],
'merge' => get_called_class(),
'id' => '$id',
'select_all' => '$select_all'
);
$documents[$prefix.$file['name']]['url'] = urldecode(http_build_query($edit_attributes));
if ($file['mime'] == 'message/rfc822') if ($file['mime'] == 'message/rfc822')
{ {
self::document_mail_action($documents[$prefix.$file['name']], $file); self::document_mail_action($documents[$prefix.$file['name']], $file);
} }
else if ($editable_mimes[$file['mime']])
{
self::document_editable_action($documents[$prefix.$file['name']], $file);
}
} }
} }
@ -2286,15 +2292,14 @@ abstract class Merge
private static function document_editable_action(Array &$action, $file) private static function document_editable_action(Array &$action, $file)
{ {
unset($action['postSubmit']); unset($action['postSubmit']);
$action['nm_action'] = 'location'; $edit_attributes = array(
$action['url'] = urldecode(http_build_query(array(
'menuaction' => 'collabora.EGroupware\\collabora\\Ui.merge_edit', 'menuaction' => 'collabora.EGroupware\\collabora\\Ui.merge_edit',
'document' => $file['path'], 'document' => $file['path'],
'merge' => get_called_class(), 'merge' => get_called_class(),
'id' => '$id', 'id' => '$id',
'select_all' => '$select_all' 'select_all' => '$select_all'
))); );
$action['target'] = '_blank'; $action['url'] = urldecode(http_build_query($edit_attributes));
} }
/** /**
@ -2331,6 +2336,167 @@ abstract class Merge
return lang("Document '%1' does not exist or is not readable for you!",$document); return lang("Document '%1' does not exist or is not readable for you!",$document);
} }
/**
* 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.
*/
public static function merge_entries()
{
if (class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge'))
{
$document_merge = new $_REQUEST['merge']();
}
else
{
$document_merge = new Api\Contacts\Merge();
}
if(($error = $document_merge->check_document($_REQUEST['document'],'')))
{
$response->error($error);
return;
}
$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 = '';
$result = $document_merge->merge_file($_REQUEST['document'], $ids, $filename, '', $header);
if(!is_file($result) || !is_readable($result))
{
throw new Api\Exception\AssertionFailed("Unable to generate merge file\n". $result);
}
// Put it into the vfs using user's configured home dir if writable,
// or expected home dir (/home/username) if not
$target = $_target = (Vfs::is_writable(Vfs::get_home_dir()) ?
Vfs::get_home_dir() :
"/home/{$GLOBALS['egw_info']['user']['account_lid']}"
)."/$filename";
$dupe_count = 0;
while(is_file(Vfs::PREFIX.$target))
{
$dupe_count++;
$target = Vfs::dirname($_target) . '/' .
pathinfo($filename, PATHINFO_FILENAME) .
' ('.($dupe_count + 1).')' . '.' .
pathinfo($filename, PATHINFO_EXTENSION);
}
copy($result, Vfs::PREFIX.$target);
unlink($result);
// Find out what to do with it
$editable_mimes = array();
try {
if (class_exists('EGroupware\\collabora\\Bo') &&
$GLOBALS['egw_info']['user']['apps']['collabora'] &&
($discovery = \EGroupware\collabora\Bo::discover()) &&
$GLOBALS['egw_info']['user']['preferences']['filemanager']['merge_open_handler'] != 'download'
)
{
$editable_mimes = $discovery;
}
}
catch (\Exception $e)
{
// ignore failed discovery
unset($e);
}
if($editable_mimes[Vfs::mime_content_type($target)])
{
\Egroupware\Api\Egw::redirect_link('/index.php', array(
'menuaction' => 'collabora.EGroupware\\Collabora\\Ui.editor',
'path'=> $target
));
}
else
{
\Egroupware\Api\Egw::redirect_link(Vfs::download_url($target));
}
}
/**
* Get all ids for when they try to do 'Select All', then merge into document
*
* @param Api\Contacts\Merge $merge App-specific merge object
*/
protected function get_all_ids(Api\Storage\Merge $merge)
{
$ids = array();
$locations = array('index', 'session_data');
// Get app
list($appname, $_merge) = explode('_', get_class($merge));
if($merge instanceOf Api\Contacts\Merge)
{
$appname = 'addressbook';
}
switch(get_class($merge))
{
case \calendar_merge::class:
$ui_class = 'calendar_uilist';
$locations = array('calendar_list');
break;
case \projectmanager_merge::class;
$ui_class = 'projectmanager_ui';
$locations = array('project_list');
break;
default:
$ui_class = $appname . '_ui';
break;
}
// Ask app
if(class_exists($ui_class))
{
$ui = new $ui_class();
if( method_exists($ui_class, 'get_all_ids') )
{
return $ui->get_all_ids();
}
// Try cache
if( method_exists($ui_class, 'get_rows'))
{
foreach($locations as $location)
{
$session = Api\Cache::getSession($appname, $location);
if($session && $session['row_id'])
{
break;
}
}
$rows = $readonlys = array();
@set_time_limit(0); // switch off the execution time limit, as it's for big selections to small
$session['num_rows'] = -1; // all
$ui->get_rows($session, $rows, $readonlys);
foreach($rows as $row_number => $row)
{
if(!is_numeric($row_number)) continue;
$row_id = $row[$session['row_id'] ? $session['row_id'] : 'id'];
switch (get_class($merge))
{
case \calendar_merge::class:
$explody = explode(':',$row_id);
$ids[] = array('id' => $explody[0], 'recur_date' => $explody[1]);
break;
case \timesheet_merge::class:
// Skip the rows with totalss
if(!is_numeric($row_id)) continue 2; // +1 for switch
// Fall through
default:
$ids[] = $row_id;
}
}
}
}
return $ids;
}
/** /**
* Get a list of supported extentions * Get a list of supported extentions
*/ */

View File

@ -27,6 +27,7 @@ class calendar_merge extends Api\Storage\Merge
var $public_functions = array( var $public_functions = array(
'download_by_request' => true, 'download_by_request' => true,
'show_replacements' => true, 'show_replacements' => true,
'merge_entries' => true
); );
// Object for getting calendar info // Object for getting calendar info

View File

@ -27,6 +27,7 @@ class filemanager_merge extends Api\Storage\Merge
*/ */
var $public_functions = array( var $public_functions = array(
'show_replacements' => true, 'show_replacements' => true,
'merge_entries' => true
); );
/** /**

View File

@ -28,6 +28,7 @@ class infolog_merge extends Api\Storage\Merge
var $public_functions = array( var $public_functions = array(
'download_by_request' => true, 'download_by_request' => true,
'show_replacements' => true, 'show_replacements' => true,
'merge_entries' => true,
); );
/** /**

View File

@ -46,6 +46,7 @@ delete this entry resources de Diesen Eintrag löschen
deleted resources de gelöscht deleted resources de gelöscht
description (short) resources de Kurzbeschreibung description (short) resources de Kurzbeschreibung
direct booking permissions resources de Erlaubnis direkt zu buchen direct booking permissions resources de Erlaubnis direkt zu buchen
do you really want to delete this resource? resources de Wollen Sie diese Ressource wirklich löschen?
don't use vfs (this will need a symlink --> see readme) resources de Vfs nicht benutzen. (Dies erfordert einen symlink --> siehe README) don't use vfs (this will need a symlink --> see readme) resources de Vfs nicht benutzen. (Dies erfordert einen symlink --> siehe README)
edit this entry resources de Diesen Eintrag bearbeiten edit this entry resources de Diesen Eintrag bearbeiten
event start resources de Ereignis startet event start resources de Ereignis startet

View File

@ -46,6 +46,7 @@ delete this entry resources en Delete this entry
deleted resources en deleted deleted resources en deleted
description (short) resources en Short description description (short) resources en Short description
direct booking permissions resources en Direct booking permissions direct booking permissions resources en Direct booking permissions
do you really want to delete this resource? resources en Do you really want to delete this resource?
don't use vfs (this will need a symlink --> see readme) resources en Don't use vfs (this will need a symlink --> see README) don't use vfs (this will need a symlink --> see readme) resources en Don't use vfs (this will need a symlink --> see README)
edit this entry resources en Edit this entry edit this entry resources en Edit this entry
event start resources en Event start event start resources en Event start

View File

@ -200,7 +200,7 @@
<button label="Apply" id="button[apply]" image="apply" background_image="1"/> <button label="Apply" id="button[apply]" image="apply" background_image="1"/>
<button label="Cancel" id="button[cancel]" onclick="window.close();" image="cancel" background_image="1"/> <button label="Cancel" id="button[cancel]" onclick="window.close();" image="cancel" background_image="1"/>
</hbox> </hbox>
<button align="right" label="Delete" id="button[delete]" onclick="et2_dialog.confirm(widget,'Do you really want do delte this resource?','Delete')" image="delete" background_image="1"/> <button align="right" label="Delete" id="button[delete]" onclick="et2_dialog.confirm(widget,'Do you really want to delete this resource?','Delete')" image="delete" background_image="1"/>
</row> </row>
</rows> </rows>
</grid> </grid>

View File

@ -82,7 +82,7 @@
<button label="Book" onclick="window.open(egw::link('/index.php','menuaction=calendar.calendar_uiforms.edit&amp;participants=r$cont[res_id]'),'','dependent=yes,width=750,height=400,location=no,menubar=no,toolbar=no,scrollbars=yes,status=yes');" id="btn_book" statustext="Book this resource"/> <button label="Book" onclick="window.open(egw::link('/index.php','menuaction=calendar.calendar_uiforms.edit&amp;participants=r$cont[res_id]'),'','dependent=yes,width=750,height=400,location=no,menubar=no,toolbar=no,scrollbars=yes,status=yes');" id="btn_book" statustext="Book this resource"/>
</hbox> </hbox>
<hbox align="right"> <hbox align="right">
<button label="Delete" onclick="return confirm('Do you really want do delte this resource?');" id="btn_delete"/> <button label="Delete" onclick="return confirm('Do you really want to delete this resource?');" id="btn_delete"/>
</hbox> </hbox>
</hbox> </hbox>
</row> </row>

View File

@ -143,7 +143,7 @@
<row class="dialogHeadbar" > <row class="dialogHeadbar" >
<hbox> <hbox>
<button id="button[apply]" image="apply" background_image="1"/> <button id="button[apply]" image="apply" background_image="1"/>
<button id="button[delete]" onclick="et2_dialog.confirm(widget,'Do you really want do delte this resource?','Delete')" image="delete" background_image="1"/> <button id="button[delete]" onclick="et2_dialog.confirm(widget,'Do you really want to delete this resource?','Delete')" image="delete" background_image="1"/>
<textbox type="integer" id="res_id" readonly="true" class="entry_id"/> <textbox type="integer" id="res_id" readonly="true" class="entry_id"/>
</hbox> </hbox>
</row> </row>

View File

@ -29,6 +29,7 @@ class timesheet_merge extends Api\Storage\Merge
'download_by_request' => true, 'download_by_request' => true,
'show_replacements' => true, 'show_replacements' => true,
'timesheet_replacements' => true, 'timesheet_replacements' => true,
'merge_entries' => true
); );
/** /**

View File

@ -239,7 +239,10 @@ class timesheet_ui extends timesheet_bo
if ($this->data['old_pm_id']) if ($this->data['old_pm_id'])
{ {
Link::unlink2(0,TIMESHEET_APP,$content['link_to']['to_id'],0,'projectmanager',$this->data['old_pm_id']); Link::unlink2(0,TIMESHEET_APP,$content['link_to']['to_id'],0,'projectmanager',$this->data['old_pm_id']);
unset($content['link_to']['to_id']['projectmanager:'.$this->data['old_pm_id']]); if(is_array($content['link_to']['to_id']))
{
unset($content['link_to']['to_id']['projectmanager:' . $this->data['old_pm_id']]);
}
unset($this->data['old_pm_id']); unset($this->data['old_pm_id']);
} }
} }