mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-25 09:23:28 +01:00
Merge remote-tracking branch 'origin/master' into upstream_master
# Conflicts: # api/js/jsapi/egw_app.ts
This commit is contained in:
commit
5dc07b36c6
@ -3250,8 +3250,6 @@ class addressbook_ui extends addressbook_bo
|
||||
// need to load list's app.js now, as exec calls header before other app can include it
|
||||
// Framework::includeJS('/'.$crm_list.'/js/app.js');
|
||||
|
||||
// Load CRM code
|
||||
Framework::includeJS('.','CRM','addressbook');
|
||||
$content['view_sidebox'] = addressbook_hooks::getViewDOMID($content['id'], $crm_list);
|
||||
$this->tmpl->exec('addressbook.addressbook_ui.view',$content,$sel_options,$readonlys,array(
|
||||
'id' => $content['id'],
|
||||
|
@ -1110,6 +1110,62 @@ class AddressbookApp extends EgwApp
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email addresses from selected contacts
|
||||
*
|
||||
* @param selected
|
||||
* @param {string[]} email_fields
|
||||
* @param {string} name_field
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async _getEmails(selected, email_fields = ["email"], name_field = 'n_fn') : Promise<string[]>
|
||||
{
|
||||
if(email_fields.length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check for all selected, don't resolve until all done
|
||||
let nm = this.et2.getWidgetById('nm');
|
||||
let all = new Promise(function(resolve)
|
||||
{
|
||||
let fetching = fetchAll(selected, nm, ids => {resolve(ids.map(function(num) {return {id: 'addressbook::' + num};}))});
|
||||
if(!fetching)
|
||||
{
|
||||
resolve(selected);
|
||||
}
|
||||
});
|
||||
let awaited = await all;
|
||||
|
||||
// Go through selected & pull email addresses from data
|
||||
let emails = [];
|
||||
for(let i = 0; i < awaited.length; i++)
|
||||
{
|
||||
// Pull data from global cache
|
||||
const data = egw.dataGetUIDdata(awaited[i].id) || {data: {}};
|
||||
let emailAddresses = email_fields.map(field =>
|
||||
{
|
||||
return data.data[field];
|
||||
})
|
||||
|
||||
// prefix email with full name
|
||||
let personal = data.data[name_field] || '';
|
||||
if(personal.match(/[^a-z0-9. -]/i))
|
||||
{
|
||||
personal = '"' + personal.replace(/"/, '\\"') + '"';
|
||||
}
|
||||
|
||||
//remove comma in personal as it will confilict with mail content comma seperator in the process
|
||||
personal = personal.replace(/,/g, '');
|
||||
|
||||
emailAddresses.forEach(mail =>
|
||||
{
|
||||
emails.push((personal ? personal + ' <' : '') + mail + (personal ? '>' : ''));
|
||||
});
|
||||
}
|
||||
return emails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the selected contacts into the target document.
|
||||
*
|
||||
|
63
addressbook/templates/kdots/view.xet
Normal file
63
addressbook/templates/kdots/view.xet
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2.0//EN" "https://www.egroupware.org/etemplate2.0.dtd">
|
||||
<overlay>
|
||||
<template id="addressbook.view" version="1.9.001" slot="left">
|
||||
<grid class="addressbook_view" width="100%">
|
||||
<columns>
|
||||
<column width="70"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row span="all">
|
||||
<et2-hbox>
|
||||
<et2-lavatar src="@photo" lname="@n_fn"></et2-lavatar>
|
||||
<et2-vbox>
|
||||
<et2-description id="n_fn" class="addressbook_sidebox_name"></et2-description>
|
||||
<et2-description id="org_name" class="addressbook_sidebox_org"></et2-description>
|
||||
<et2-description id="org_unit"></et2-description>
|
||||
<et2-description id="adr_one_locality"></et2-description>
|
||||
</et2-vbox>
|
||||
</et2-hbox>
|
||||
</row>
|
||||
<row>
|
||||
<et2-description span="2" value="Phone numbers" class="addressbook_sidebox_header"></et2-description>
|
||||
</row>
|
||||
<row>
|
||||
<et2-description for="tel_work" value="Business"></et2-description>
|
||||
<et2-url-phone id="tel_work" readonly="true"></et2-url-phone>
|
||||
</row>
|
||||
<row>
|
||||
<et2-description for="tel_cell" value="Mobile phone"></et2-description>
|
||||
<et2-url-phone id="tel_cell" readonly="true"></et2-url-phone>
|
||||
</row>
|
||||
<row>
|
||||
<et2-description for="tel_home" value="Private"></et2-description>
|
||||
<et2-url-phone id="tel_home" readonly="true"></et2-url-phone>
|
||||
</row>
|
||||
<row>
|
||||
<et2-description for="tel_fax" value="Fax"></et2-description>
|
||||
<et2-url-phone id="tel_fax" readonly="true"></et2-url-phone>
|
||||
</row>
|
||||
<row>
|
||||
<et2-description span="2" value="EMail & Internet" class="addressbook_sidebox_header"></et2-description>
|
||||
</row>
|
||||
<row>
|
||||
<et2-description for="email" value="EMail"></et2-description>
|
||||
<et2-url-email id="email" readonly="true"></et2-url-email>
|
||||
</row>
|
||||
<row>
|
||||
<et2-description for="url" value="URL"></et2-description>
|
||||
<et2-url id="url" readonly="true"></et2-url>
|
||||
</row>
|
||||
<row class="toolbox">
|
||||
<et2-hbox>
|
||||
<et2-button id="button[edit]" label="open" image="edit" onclick="app.addressbook.view_actions"></et2-button>
|
||||
<et2-button id="button[copy]" label="copy" image="copy" onclick="app.addressbook.view_actions"></et2-button>
|
||||
<et2-button id="button[close]" statustext="close" readonly="false" image="close" onclick="app.addressbook.view_actions"></et2-button>
|
||||
<et2-button id="button[delete]" label="delete" image="delete" onclick="app.addressbook.view_actions" noSubmit="true"></et2-button>
|
||||
</et2-hbox>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</template>
|
||||
</overlay>
|
@ -423,7 +423,14 @@ class AdminApp extends EgwApp
|
||||
if(!_data || _data.type != undefined) return;
|
||||
|
||||
// Insert the content, etemplate will load into it
|
||||
jQuery(this.ajax_target.getDOMNode()).append(typeof _data === 'string' ? _data : _data[0]);
|
||||
if(typeof _data === "string" || typeof _data[0] !== "undefined")
|
||||
{
|
||||
jQuery(this.ajax_target.getDOMNode()).append(typeof _data === 'string' ? _data : _data[0]);
|
||||
}
|
||||
else if(typeof _data.DOMNodeID == "string")
|
||||
{
|
||||
this.ajax_target.setAttribute("id", _data.DOMNodeID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +83,7 @@
|
||||
</grid>
|
||||
</template>
|
||||
<template id="admin.index" template="" lang="" group="0" version="1.9.001">
|
||||
<tree autoloading="admin_ui::ajax_tree" id="tree" onclick="app.admin.run" parent_node="admin_tree_target" std_images="bullet"/>
|
||||
<tree slot="left" autoloading="admin_ui::ajax_tree" id="tree" onclick="app.admin.run" parent_node="admin_tree_target" std_images="bullet"/>
|
||||
<nextmatch id="nm" template="admin.index.rows" header_left="admin.index.add"/>
|
||||
<nextmatch id="groups" template="admin.index.group" class="hide"/>
|
||||
<iframe frameborder="1" height="100%" id="iframe" scrolling="auto" width="100%" disabled="true"/>
|
||||
|
145
api/js/etemplate/Et2Dialog/Et2MergeDialog.ts
Normal file
145
api/js/etemplate/Et2Dialog/Et2MergeDialog.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import {customElement} from "lit/decorators/custom-element.js";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {css, html, LitElement} from "lit";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import {Et2VfsSelectDialog} from "../Et2Vfs/Et2VfsSelectDialog";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {Et2Dialog} from "./Et2Dialog";
|
||||
import {state} from "lit/decorators/state.js";
|
||||
|
||||
@customElement("et2-merge-dialog")
|
||||
export class Et2MergeDialog extends Et2Widget(LitElement)
|
||||
{
|
||||
static get styles()
|
||||
{
|
||||
return [
|
||||
super.styles,
|
||||
shoelace,
|
||||
css`
|
||||
:host {
|
||||
}
|
||||
|
||||
et2-details::part(content) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@property()
|
||||
application : string
|
||||
|
||||
@property()
|
||||
path : string
|
||||
|
||||
// Can't merge "& send" if no email template selected
|
||||
@state()
|
||||
canEmail = false;
|
||||
|
||||
private get dialog() : Et2VfsSelectDialog
|
||||
{
|
||||
return <Et2VfsSelectDialog><unknown>this.shadowRoot.querySelector("et2-vfs-select-dialog");
|
||||
}
|
||||
|
||||
public async getComplete() : Promise<{
|
||||
documents : { path : string, mime : string }[],
|
||||
options : { [key : string] : string | boolean }
|
||||
}>
|
||||
{
|
||||
await this.updateComplete;
|
||||
const [button, value] = await this.dialog.getComplete();
|
||||
|
||||
if(!button)
|
||||
{
|
||||
return {documents: [], options: this.optionValues};
|
||||
}
|
||||
|
||||
const documents = [];
|
||||
Array.from(<ArrayLike<string>>value).forEach(value =>
|
||||
{
|
||||
const fileInfo = this.dialog.fileInfo(value) ?? [];
|
||||
documents.push({path: value, mime: fileInfo.mime})
|
||||
});
|
||||
let options = this.optionValues;
|
||||
if(button == Et2Dialog.OK_BUTTON)
|
||||
{
|
||||
options.download = true;
|
||||
}
|
||||
return {documents: documents, options: options};
|
||||
}
|
||||
|
||||
public get optionValues()
|
||||
{
|
||||
const optionValues = {
|
||||
download: false
|
||||
};
|
||||
this.dialog.querySelectorAll(":not([slot='footer'])").forEach(e =>
|
||||
{
|
||||
if(typeof e.getValue == "function")
|
||||
{
|
||||
optionValues[e.getAttribute("id")] = e.getValue() === "true" ? true : e.getValue();
|
||||
}
|
||||
});
|
||||
return optionValues;
|
||||
}
|
||||
|
||||
private option(component_name)
|
||||
{
|
||||
return this.dialog.querySelector("et2-details > [id='" + component_name + "']");
|
||||
}
|
||||
|
||||
protected handleFileSelect(event)
|
||||
{
|
||||
// Disable PDF checkbox for only email files selected
|
||||
let canPDF = false;
|
||||
const oldCanEmail = this.canEmail;
|
||||
this.canEmail = false;
|
||||
|
||||
this.dialog.value.forEach(path =>
|
||||
{
|
||||
if(this.dialog.fileInfo(path).mime !== "message/rfc822")
|
||||
{
|
||||
canPDF = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.canEmail = true;
|
||||
}
|
||||
});
|
||||
this.option("pdf").disabled = !canPDF;
|
||||
this.requestUpdate("canEmail", oldCanEmail);
|
||||
}
|
||||
|
||||
|
||||
render()
|
||||
{
|
||||
return html`
|
||||
<et2-vfs-select-dialog
|
||||
class=egw_app_merge_document"
|
||||
path=${this.path}
|
||||
multiple="true"
|
||||
buttonLabel=${this.egw().lang("Download")}
|
||||
.title="${this.egw().lang("Insert in document")}"
|
||||
.open=${true}
|
||||
@et2-select=${this.handleFileSelect}
|
||||
>
|
||||
${this.canEmail ?
|
||||
html`
|
||||
<et2-button slot="footer" id="send" label=${this.egw().lang("Merge & send")} image="mail"
|
||||
noSubmit="true"></et2-button> ` :
|
||||
html`
|
||||
<et2-button slot="footer" id="send" label=${this.egw().lang("Merge")} image="etemplate/merge"
|
||||
noSubmit="true"></et2-button>`
|
||||
}
|
||||
<et2-details>
|
||||
<span slot="summary">${this.egw().lang("Merge options")}</span>
|
||||
<et2-checkbox label=${this.egw().lang("Save as PDF")} id="pdf"></et2-checkbox>
|
||||
<et2-checkbox id="link"
|
||||
label="${this.egw().lang("Link to each entry")}"
|
||||
></et2-checkbox>
|
||||
<et2-checkbox label=${this.egw().lang("Merge individually")} id="individual"></et2-checkbox>
|
||||
</et2-details>
|
||||
</et2-vfs-select-dialog>`;
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import {Et2Image} from "../Et2Image/Et2Image";
|
||||
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
||||
import {SlMenuItem} from "@shoelace-style/shoelace";
|
||||
import {cssImage} from "../Et2Widget/Et2Widget";
|
||||
import {Favorite} from "./Favorite";
|
||||
|
||||
/**
|
||||
* Favorites widget, designed for use in the nextmatch header
|
||||
@ -108,7 +109,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
|
||||
// Favorites are prefixed in preferences
|
||||
public static readonly PREFIX = "favorite_";
|
||||
protected static readonly ADD_VALUE = "~add~";
|
||||
static readonly ADD_VALUE = "~add~";
|
||||
|
||||
private favSortedList : any = [];
|
||||
private _preferred : string;
|
||||
@ -193,8 +194,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
if(changedProperties.has("app"))
|
||||
{
|
||||
this._preferred = <string>this.egw().preference(this.defaultPref, this.app);
|
||||
this.__select_options = this._load_favorites(this.app);
|
||||
this.requestUpdate("select_options");
|
||||
this._load_favorites(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,87 +205,26 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
*/
|
||||
_load_favorites(app)
|
||||
{
|
||||
|
||||
// Default blank filter
|
||||
let favorites : any = {
|
||||
'blank': {
|
||||
name: this.egw().lang("No filters"),
|
||||
state: {}
|
||||
}
|
||||
};
|
||||
|
||||
// Load saved favorites
|
||||
this.favSortedList = [];
|
||||
let preferences : any = this.egw().preference("*", app);
|
||||
for(let pref_name in preferences)
|
||||
Favorite.load(this.egw(), app).then((favorites) =>
|
||||
{
|
||||
if(pref_name.indexOf(Et2Favorites.PREFIX) == 0 && typeof preferences[pref_name] == 'object')
|
||||
let options = [];
|
||||
Object.keys(favorites).forEach((name) =>
|
||||
{
|
||||
let name = pref_name.substr(Et2Favorites.PREFIX.length);
|
||||
favorites[name] = preferences[pref_name];
|
||||
// Keep older favorites working - they used to store nm filters in 'filters',not state
|
||||
if(preferences[pref_name]["filters"])
|
||||
{
|
||||
favorites[pref_name]["state"] = preferences[pref_name]["filters"];
|
||||
}
|
||||
}
|
||||
if(pref_name == 'fav_sort_pref')
|
||||
options.push(Object.assign({value: name, label: favorites[name].name || name}, favorites[name]));
|
||||
})
|
||||
// Only add 'Add current' if we have a nextmatch
|
||||
if(this._nextmatch)
|
||||
{
|
||||
this.favSortedList = preferences[pref_name];
|
||||
//Make sure sorted list is always an array, seems some old fav are not array
|
||||
if(!Array.isArray(this.favSortedList))
|
||||
{
|
||||
this.favSortedList = this.favSortedList.split(',');
|
||||
}
|
||||
options.push({value: Et2Favorites.ADD_VALUE, label: this.egw().lang('Add current')});
|
||||
}
|
||||
}
|
||||
|
||||
for(let name in favorites)
|
||||
{
|
||||
if(this.favSortedList.indexOf(name) < 0)
|
||||
{
|
||||
this.favSortedList.push(name);
|
||||
}
|
||||
}
|
||||
this.egw().set_preference(this.app, 'fav_sort_pref', this.favSortedList);
|
||||
if(this.favSortedList.length > 0)
|
||||
{
|
||||
let sortedListObj = {};
|
||||
|
||||
for(let i = 0; i < this.favSortedList.length; i++)
|
||||
{
|
||||
if(typeof favorites[this.favSortedList[i]] != 'undefined')
|
||||
{
|
||||
sortedListObj[this.favSortedList[i]] = favorites[this.favSortedList[i]];
|
||||
}
|
||||
else
|
||||
{
|
||||
this.favSortedList.splice(i, 1);
|
||||
this.egw().set_preference(this.app, 'fav_sort_pref', this.favSortedList);
|
||||
}
|
||||
}
|
||||
favorites = Object.assign(sortedListObj, favorites);
|
||||
}
|
||||
|
||||
let options = [];
|
||||
Object.keys(favorites).forEach((name) =>
|
||||
{
|
||||
options.push(Object.assign({value: name, label: favorites[name].name || name}, favorites[name]));
|
||||
})
|
||||
// Only add 'Add current' if we have a nextmatch
|
||||
if(this._nextmatch)
|
||||
{
|
||||
options.push({value: Et2Favorites.ADD_VALUE, label: this.egw().lang('Add current')});
|
||||
}
|
||||
|
||||
this.requestUpdate("select_options");
|
||||
return options;
|
||||
this.__select_options = options
|
||||
this.requestUpdate("select_options");
|
||||
});
|
||||
}
|
||||
|
||||
public load_favorites(app)
|
||||
{
|
||||
this.__select_options = this._load_favorites(app);
|
||||
this.requestUpdate("select_options");
|
||||
this._load_favorites(app);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -349,7 +288,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
}
|
||||
this._value = ev.detail.item.value;
|
||||
|
||||
this._apply_favorite(ev.detail.item.value);
|
||||
Favorite.applyFavorite(this.egw(), this.app, ev.detail.item.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -360,7 +299,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
*/
|
||||
protected _handleClick(event : MouseEvent)
|
||||
{
|
||||
this._apply_favorite(this.preferred);
|
||||
Favorite.applyFavorite(this.egw, this.app, this.preferred);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -407,9 +346,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
trash.remove();
|
||||
|
||||
// Delete preference server side
|
||||
let request = this.egw().json("EGroupware\\Api\\Framework::ajax_set_favorite",
|
||||
[this.app, fav.name, "delete", "" + fav.group, '']).sendRequest();
|
||||
request.then(response =>
|
||||
Favorite.remove(this.egw(), this.app, line.value).then(response =>
|
||||
{
|
||||
line.classList.remove("loading");
|
||||
|
||||
@ -442,24 +379,6 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a favorite to the app or nextmatch
|
||||
*
|
||||
* @param {string} favorite_name
|
||||
* @protected
|
||||
*/
|
||||
protected _apply_favorite(favorite_name : string)
|
||||
{
|
||||
let fav = favorite_name == "blank" ? {} : this.favoriteByID(favorite_name);
|
||||
// use app[appname].setState if available to allow app to overwrite it (eg. change to non-listview in calendar)
|
||||
//@ts-ignore TS doesn't know about window.app
|
||||
if(typeof window.app[this.app] != 'undefined')
|
||||
{
|
||||
//@ts-ignore TS doesn't know about window.app
|
||||
window.app[this.app].setState(fav);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the nextmatch to filter
|
||||
* From et2_INextmatchHeader interface
|
||||
@ -477,8 +396,7 @@ export class Et2Favorites extends Et2DropdownButton implements et2_INextmatchHea
|
||||
}
|
||||
|
||||
// Re-generate filter list so we can add 'Add current'
|
||||
this.__select_options = this._load_favorites(this.app);
|
||||
this.requestUpdate("select_options");
|
||||
this._load_favorites(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
|
5
api/js/etemplate/Et2Favorites/Et2FavoritesMenu.md
Normal file
5
api/js/etemplate/Et2Favorites/Et2FavoritesMenu.md
Normal file
@ -0,0 +1,5 @@
|
||||
```html:preview
|
||||
<et2-favorites-menu>
|
||||
</et2-favorites-menu>
|
||||
```
|
||||
|
139
api/js/etemplate/Et2Favorites/Et2FavoritesMenu.ts
Normal file
139
api/js/etemplate/Et2Favorites/Et2FavoritesMenu.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import {css, html, LitElement, nothing, TemplateResult} from "lit";
|
||||
import {customElement} from "lit/decorators/custom-element.js";
|
||||
import {Et2Widget} from "../Et2Widget/Et2Widget";
|
||||
import {Favorite} from "./Favorite";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {until} from "lit/directives/until.js";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
|
||||
/**
|
||||
* @summary A menu listing a user's favorites. Populated from the user's preferences.
|
||||
*
|
||||
* @dependency sl-menu
|
||||
* @dependency sl-menu-item
|
||||
* @dependency sl-menu-label
|
||||
* @dependency et2-image
|
||||
*
|
||||
* @slot - Add additional menu items
|
||||
*/
|
||||
@customElement("et2-favorites-menu")
|
||||
export class Et2FavoritesMenu extends Et2Widget(LitElement)
|
||||
{
|
||||
static get styles()
|
||||
{
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
:host {
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
et2-image[src="trash"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
sl-menu-item:hover et2-image[src="trash"] {
|
||||
display: initial;
|
||||
}`
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* The current application we're showing favorites for.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
@property()
|
||||
application : string;
|
||||
|
||||
private favorites : { [name : string] : Favorite } = {
|
||||
'blank': {
|
||||
name: typeof this.egw()?.lang == "function" ? this.egw().lang("No filters") : "No filters",
|
||||
state: {},
|
||||
group: false
|
||||
}
|
||||
};
|
||||
private loadingPromise = Promise.resolve();
|
||||
|
||||
connectedCallback()
|
||||
{
|
||||
super.connectedCallback();
|
||||
|
||||
if(this.application)
|
||||
{
|
||||
this.loadingPromise = Favorite.load(this.egw(), this.application).then((favorites) =>
|
||||
{
|
||||
this.favorites = favorites;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSelect(event)
|
||||
{
|
||||
Favorite.applyFavorite(this.egw(), this.application, event.detail.item.value);
|
||||
}
|
||||
|
||||
handleDelete(event)
|
||||
{
|
||||
// Don't trigger click
|
||||
event.stopPropagation();
|
||||
|
||||
const menuItem = event.target.closest("sl-menu-item");
|
||||
menuItem.setAttribute("loading", "");
|
||||
|
||||
const favoriteName = menuItem.value;
|
||||
|
||||
// Remove from server
|
||||
Favorite.remove(this.egw(), this.application, favoriteName).then(() =>
|
||||
{
|
||||
// Remove from widget
|
||||
delete this.favorites[favoriteName];
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected menuItemTemplate(name : string, favorite : Favorite) : TemplateResult
|
||||
{
|
||||
let is_admin = (typeof this.egw()?.app == "function") && (typeof this.egw()?.app('admin') != "undefined");
|
||||
|
||||
//@ts-ignore option.group does not exist
|
||||
let icon = (favorite.group !== false && !is_admin || ['blank', '~add~'].includes(name)) ? "" : html`
|
||||
<et2-image slot="suffix" src="trash" icon @click=${this.handleDelete}
|
||||
statustext="${this.egw()?.lang("Delete") ?? "Delete"}"></et2-image>`;
|
||||
|
||||
return html`
|
||||
<sl-menu-item value="${name}">
|
||||
${icon}
|
||||
${favorite.name}
|
||||
</sl-menu-item>`;
|
||||
}
|
||||
|
||||
protected loadingTemplate()
|
||||
{
|
||||
return html`
|
||||
<sl-menu-item loading>${typeof this.egw()?.lang == "function" ? this.egw().lang("Loading") : "Loading"}
|
||||
</sl-menu-item>`;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
let content = this.loadingPromise.then(() =>
|
||||
{
|
||||
return html`
|
||||
<sl-menu
|
||||
@sl-select=${this.handleSelect}
|
||||
>
|
||||
${this.label ? html`
|
||||
<sl-menu-label>${this.label}</sl-menu-label>` : nothing}
|
||||
${repeat(Object.keys(this.favorites), (i) => this.menuItemTemplate(i, this.favorites[i]))}
|
||||
<slot></slot>
|
||||
</sl-menu>
|
||||
`;
|
||||
});
|
||||
return html`
|
||||
${until(content, this.loadingTemplate())}
|
||||
`;
|
||||
}
|
||||
}
|
109
api/js/etemplate/Et2Favorites/Favorite.ts
Normal file
109
api/js/etemplate/Et2Favorites/Favorite.ts
Normal file
@ -0,0 +1,109 @@
|
||||
export class Favorite
|
||||
{
|
||||
name : string
|
||||
state : object
|
||||
group : number | false
|
||||
|
||||
// Favorites are prefixed in preferences
|
||||
public static readonly PREFIX = "favorite_";
|
||||
public static readonly ADD_VALUE = "~add~";
|
||||
|
||||
/**
|
||||
* Load favorites from preferences
|
||||
*
|
||||
* @param app String Load favorites from this application
|
||||
*/
|
||||
static async load(egw, app : string) : Promise<{ [name : string] : Favorite }>
|
||||
{
|
||||
// Default blank filter
|
||||
let favorites : { [name : string] : Favorite } = {
|
||||
'blank': {
|
||||
name: egw.lang("No filters"),
|
||||
state: {},
|
||||
group: false
|
||||
}
|
||||
};
|
||||
|
||||
// Load saved favorites
|
||||
let sortedList = [];
|
||||
let preferences : any = await egw.preference("*", app, true);
|
||||
for(let pref_name in preferences)
|
||||
{
|
||||
if(pref_name.indexOf(Favorite.PREFIX) == 0 && typeof preferences[pref_name] == 'object')
|
||||
{
|
||||
let name = pref_name.substr(Favorite.PREFIX.length);
|
||||
favorites[name] = preferences[pref_name];
|
||||
// Keep older favorites working - they used to store nm filters in 'filters',not state
|
||||
if(preferences[pref_name]["filters"])
|
||||
{
|
||||
favorites[pref_name]["state"] = preferences[pref_name]["filters"];
|
||||
}
|
||||
}
|
||||
if(pref_name == 'fav_sort_pref')
|
||||
{
|
||||
sortedList = preferences[pref_name];
|
||||
//Make sure sorted list is always an array, seems some old fav are not array
|
||||
if(!Array.isArray(sortedList) && typeof sortedList == "string")
|
||||
{
|
||||
// @ts-ignore What's the point of a typecheck if IDE still errors
|
||||
sortedList = sortedList.split(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(let name in favorites)
|
||||
{
|
||||
if(sortedList.indexOf(name) < 0)
|
||||
{
|
||||
sortedList.push(name);
|
||||
}
|
||||
}
|
||||
egw.set_preference(app, 'fav_sort_pref', sortedList);
|
||||
if(sortedList.length > 0)
|
||||
{
|
||||
let sortedListObj = {};
|
||||
|
||||
for(let i = 0; i < sortedList.length; i++)
|
||||
{
|
||||
if(typeof favorites[sortedList[i]] != 'undefined')
|
||||
{
|
||||
sortedListObj[sortedList[i]] = favorites[sortedList[i]];
|
||||
}
|
||||
else
|
||||
{
|
||||
sortedList.splice(i, 1);
|
||||
egw.set_preference(app, 'fav_sort_pref', sortedList);
|
||||
}
|
||||
}
|
||||
favorites = Object.assign(sortedListObj, favorites);
|
||||
}
|
||||
|
||||
return favorites;
|
||||
}
|
||||
|
||||
static async applyFavorite(egw, app : string, favoriteName : string)
|
||||
{
|
||||
const favorites = await Favorite.load(egw, app);
|
||||
let fav = favoriteName == "blank" ? {} : favorites[favoriteName] ?? {};
|
||||
// use app[appname].setState if available to allow app to overwrite it (eg. change to non-listview in calendar)
|
||||
//@ts-ignore TS doesn't know about window.app
|
||||
if(typeof window.app[app] != 'undefined')
|
||||
{
|
||||
//@ts-ignore TS doesn't know about window.app
|
||||
window.app[app].setState(fav);
|
||||
}
|
||||
}
|
||||
|
||||
static async remove(egw, app, favoriteName)
|
||||
{
|
||||
const favorites = await Favorite.load(egw, app);
|
||||
let fav = favorites[favoriteName];
|
||||
if(!fav)
|
||||
{
|
||||
return Promise.reject("No such favorite");
|
||||
}
|
||||
|
||||
return egw.request("EGroupware\\Api\\Framework::ajax_set_favorite",
|
||||
[app, favoriteName, "delete", "" + fav.group, '']);
|
||||
}
|
||||
}
|
@ -437,7 +437,10 @@ export class Et2LinkTo extends Et2InputWidget(LitElement)
|
||||
handleEntrySelected(event)
|
||||
{
|
||||
// Could be the app, could be they selected an entry
|
||||
if(event.target == this.select._searchNode)
|
||||
if(event.target == this.select && (
|
||||
typeof this.select.value == "string" && this.select.value ||
|
||||
typeof this.select.value == "object" && this.select.value.id
|
||||
))
|
||||
{
|
||||
this.classList.add("can_link");
|
||||
this.link_button.focus();
|
||||
|
@ -184,6 +184,7 @@ export class Et2Number extends Et2Textbox
|
||||
|
||||
private handleScroll(e)
|
||||
{
|
||||
if (this.disabled) return;
|
||||
const old_value = this.value;
|
||||
let min = parseFloat(this.min ?? Number.MIN_SAFE_INTEGER);
|
||||
if(Number.isNaN(min))
|
||||
@ -208,7 +209,7 @@ export class Et2Number extends Et2Textbox
|
||||
return '';
|
||||
}
|
||||
|
||||
return html`
|
||||
return this.disabled ? '' : html`
|
||||
<et2-button-scroll class="et2-number__scrollbuttons" slot="suffix"
|
||||
part="scroll"
|
||||
@et2-scroll=${this.handleScroll}></et2-button-scroll>`;
|
||||
|
@ -966,13 +966,17 @@ export class Et2Tree extends Et2WidgetWithSelectMixin(LitElement)
|
||||
* @return {TreeItemData} node with the given _id or null
|
||||
* @private
|
||||
*/
|
||||
private _search(_id: string, data: TreeItemData[]): TreeItemData
|
||||
private _search(_id: string|number, data: TreeItemData[]): TreeItemData
|
||||
{
|
||||
let res: TreeItemData = null
|
||||
if (_id == undefined)
|
||||
{
|
||||
return null
|
||||
}
|
||||
if (typeof _id === "number")
|
||||
{
|
||||
_id = _id + "";
|
||||
}
|
||||
for (const value of data)
|
||||
{
|
||||
if (value.id === _id)
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
|
||||
import {html, LitElement, nothing, PropertyValues, TemplateResult} from "lit";
|
||||
import {html, LitElement, nothing, PropertyValues, render, TemplateResult} from "lit";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import styles from "./Et2VfsSelect.styles";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
@ -110,7 +110,8 @@ export class Et2VfsSelectDialog
|
||||
label: "Spreadsheets"
|
||||
},
|
||||
{value: "image/", label: "Images"},
|
||||
{value: "video/", label: "Videos"}
|
||||
{value: "video/", label: "Videos"},
|
||||
{value: "message/rfc822", label: "Email"}
|
||||
];
|
||||
|
||||
/** The select's help text. If you need to display HTML, use the `help-text` slot instead. */
|
||||
@ -249,11 +250,26 @@ export class Et2VfsSelectDialog
|
||||
|
||||
public setPath(path)
|
||||
{
|
||||
const oldValue = this.path;
|
||||
|
||||
// Selection doesn't stay across sub-dirs. Notify user we dropped them.
|
||||
if(this.value.length && path != oldValue)
|
||||
{
|
||||
const length = this.value.length;
|
||||
this.value = [];
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
render(html`
|
||||
<sl-alert duration="5000" closable open>
|
||||
<sl-icon slot="icon" name="info-circle"></sl-icon>
|
||||
${this.egw().lang("Selection of files can only be done in one folder. %1 files unselected.", length)}
|
||||
</sl-alert>`, <HTMLElement><unknown>this);
|
||||
});
|
||||
}
|
||||
if(path == '..')
|
||||
{
|
||||
path = this.dirname(this.path);
|
||||
}
|
||||
const oldValue = this.path;
|
||||
this._pathNode.value = this.path = path;
|
||||
this.requestUpdate("path", oldValue);
|
||||
this.currentResult = null;
|
||||
@ -745,7 +761,7 @@ export class Et2VfsSelectDialog
|
||||
image="filemanager/fav_filter" noSubmit="true"
|
||||
@click=${() => this.setPath("/apps/favorites")}
|
||||
></et2-button>
|
||||
<et2-select id="app" emptyLabel="Applications" noLang="1"
|
||||
<et2-select id="app" emptyLabel="${this.egw().lang("Applications")}" noLang="1"
|
||||
.select_options=${this._appList}
|
||||
@change=${(e) => this.setPath("/apps/" + e.target.value)}
|
||||
>
|
||||
@ -813,7 +829,7 @@ export class Et2VfsSelectDialog
|
||||
|
||||
const buttons = [
|
||||
{id: "ok", label: this.buttonLabel, image: image, button_id: Et2Dialog.OK_BUTTON},
|
||||
{id: "cancel", label: "cancel", image: "cancel", button_id: Et2Dialog.CANCEL_BUTTON}
|
||||
{id: "cancel", label: this.egw().lang("cancel"), image: "cancel", button_id: Et2Dialog.CANCEL_BUTTON}
|
||||
];
|
||||
|
||||
return html`
|
||||
@ -882,6 +898,7 @@ export class Et2VfsSelectDialog
|
||||
></et2-vfs-path>
|
||||
</div>
|
||||
${this.searchResultsTemplate()}
|
||||
<slot></slot>
|
||||
<sl-visually-hidden>
|
||||
<et2-label for="mimeFilter">${this.egw().lang("mime filter")}</et2-label>
|
||||
</sl-visually-hidden>
|
||||
@ -901,7 +918,6 @@ export class Et2VfsSelectDialog
|
||||
>
|
||||
${this.mimeOptionsTemplate()}
|
||||
</et2-select>
|
||||
<slot></slot>
|
||||
<div
|
||||
part="form-control-help-text"
|
||||
id="help-text"
|
||||
|
@ -6,6 +6,7 @@ import {state} from "lit/decorators/state.js";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
import shoelace from "../Styles/shoelace";
|
||||
import styles from "./Et2VfsSelectRow.styles";
|
||||
import {SearchResultElement} from "../Et2Widget/SearchMixin";
|
||||
|
||||
/**
|
||||
* @summary Shows one file in the Et2VfsSelectDialog list
|
||||
@ -16,7 +17,7 @@ import styles from "./Et2VfsSelectRow.styles";
|
||||
* @csspart base - The component’s base wrapper.
|
||||
* @csspart checked-icon - The checked icon, an <sl-icon> element.
|
||||
*/
|
||||
export class Et2VfsSelectRow extends Et2Widget(LitElement)
|
||||
export class Et2VfsSelectRow extends Et2Widget(LitElement) implements SearchResultElement
|
||||
{
|
||||
static get styles()
|
||||
{
|
||||
@ -43,6 +44,11 @@ export class Et2VfsSelectRow extends Et2Widget(LitElement)
|
||||
this.setAttribute('aria-selected', 'false');
|
||||
}
|
||||
|
||||
@property()
|
||||
get label()
|
||||
{
|
||||
return this.value?.label || this.value?.name || "";
|
||||
}
|
||||
private handleMouseEnter()
|
||||
{
|
||||
this.hasHover = true;
|
||||
|
@ -15,7 +15,7 @@ import {egw} from "../../jsapi/egw_global";
|
||||
registerIconLibrary('default', {
|
||||
resolver: name =>
|
||||
{
|
||||
return typeof egw !== "undefined" ? (egw.image(name) ?? `${egw.webserverUrl || ""}/node_modules/@shoelace-style/shoelace/dist/assets/icons/${name}.svg`) : ''
|
||||
return typeof egw !== "undefined" && typeof egw.image == "function" ? (egw.image(name) ?? `${egw.webserverUrl || ""}/node_modules/@shoelace-style/shoelace/dist/assets/icons/${name}.svg`) : ''
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -197,7 +197,8 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
class: "",
|
||||
valign: "top",
|
||||
span: "1",
|
||||
disabled: false
|
||||
disabled: false,
|
||||
style: ""
|
||||
};
|
||||
}
|
||||
|
||||
@ -285,6 +286,7 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
rowDataEntry["valign"] = et2_readAttrWithDefault(node, "valign", "");
|
||||
rowDataEntry["span"] = et2_readAttrWithDefault(node, "span", "1");
|
||||
rowDataEntry["part"] = et2_readAttrWithDefault(node, "part", "body");
|
||||
rowDataEntry["style"] = et2_readAttrWithDefault(node, "style", "");
|
||||
|
||||
const id = et2_readAttrWithDefault(node, "id", "");
|
||||
if(id)
|
||||
@ -549,6 +551,10 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
{
|
||||
this._getCell(cells, x, y).rowData.class = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.class);
|
||||
}
|
||||
if(this._getCell(cells, x, y).rowData.style)
|
||||
{
|
||||
this._getCell(cells, x, y).rowData.style = this.getArrayMgr("content").expandName(this._getCell(cells, x, y).rowData.style);
|
||||
}
|
||||
|
||||
}
|
||||
if(!nm && typeof cell.disabled === 'string')
|
||||
@ -771,6 +777,11 @@ export class et2_grid extends et2_DOMWidget implements et2_IDetachedDOM, et2_IAl
|
||||
tr.attr("valign", this.rowData[y].valign);
|
||||
}
|
||||
|
||||
if(this.rowData[y].style)
|
||||
{
|
||||
tr.attr("style", this.rowData[y].style);
|
||||
}
|
||||
|
||||
if(this.rowData[y].id)
|
||||
{
|
||||
tr.attr("id", this.rowData[y].id);
|
||||
@ -1226,5 +1237,6 @@ interface RowEntry
|
||||
class : string, // "",
|
||||
valign : string, // "top",
|
||||
span : string | number, // "1",
|
||||
disabled : boolean // false
|
||||
disabled : boolean, // false
|
||||
style: string
|
||||
}
|
@ -48,6 +48,12 @@ export class et2_radiobox extends et2_inputWidget
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "What should be displayed when readonly and not selected"
|
||||
},
|
||||
name: {
|
||||
"name": "Name of radio-group",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Used to group radio-buttons, defaults to id of radio-button"
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,7 +102,10 @@ export class et2_radiobox extends et2_inputWidget
|
||||
super.set_id(_id);
|
||||
|
||||
this.dom_id = this.dom_id.replace('[]', '')+'-'+this.options.set_value;
|
||||
if (this.input) this.input.attr('id', this.dom_id);
|
||||
if (this.input) {
|
||||
this.input.attr('id', this.dom_id);
|
||||
if (this.options.name) this.input.attr('name', this.options.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,7 +131,7 @@ export class et2_radiobox extends et2_inputWidget
|
||||
{
|
||||
this.getRoot().iterateOver(function(radio)
|
||||
{
|
||||
if (radio.id == this.id)
|
||||
if (radio.id == this.id && (!this.options.name || this.options.name == radio.options.name))
|
||||
{
|
||||
radio.input.prop('checked', _value == radio.options.set_value);
|
||||
}
|
||||
@ -141,7 +150,8 @@ export class et2_radiobox extends et2_inputWidget
|
||||
this.getRoot().iterateOver(function(radio)
|
||||
{
|
||||
values.push(radio.options.set_value);
|
||||
if (radio.id == this.id && radio.input && radio.input.prop('checked'))
|
||||
if (radio.id == this.id && radio.input && (!this.options.name || this.options.name == radio.options.name) &&
|
||||
radio.input.prop('checked'))
|
||||
{
|
||||
val = radio.options.set_value;
|
||||
}
|
||||
@ -486,5 +496,4 @@ export class et2_radioGroup extends et2_valueWidget implements et2_IDetachedDOM
|
||||
|
||||
}
|
||||
// No such tag as 'radiogroup', but it needs something
|
||||
et2_register_widget(et2_radioGroup, ["radiogroup"]);
|
||||
|
||||
et2_register_widget(et2_radioGroup, ["radiogroup"]);
|
@ -49,11 +49,13 @@ import './Et2Date/Et2DateTimeReadonly';
|
||||
import './Et2Date/Et2DateTimeToday';
|
||||
import './Et2Description/Et2Description';
|
||||
import './Et2Dialog/Et2Dialog';
|
||||
import './Et2Dialog/Et2MergeDialog';
|
||||
import './Et2DropdownButton/Et2DropdownButton';
|
||||
import './Et2Email/Et2Email';
|
||||
import './Expose/Et2ImageExpose';
|
||||
import './Expose/Et2DescriptionExpose';
|
||||
import './Et2Favorites/Et2Favorites';
|
||||
import './Et2Favorites/Et2FavoritesMenu';
|
||||
import './Et2Image/Et2Image';
|
||||
import './Et2Image/Et2AppIcon';
|
||||
import './Et2Avatar/Et2LAvatar';
|
||||
@ -707,6 +709,11 @@ export class etemplate2
|
||||
etemplate2._byTemplate[_name].push(this);
|
||||
|
||||
// Read the XML structure of the requested template
|
||||
if(etemplate2.templates[this.name].hasAttribute("slot"))
|
||||
{
|
||||
this.DOMContainer.setAttribute("slot", etemplate2.templates[this.name].getAttribute("slot"));
|
||||
}
|
||||
|
||||
this._widgetContainer.loadFromXML(etemplate2.templates[this.name]);
|
||||
console.timeEnd("loadFromXML");
|
||||
console.time("deferred");
|
||||
@ -1642,7 +1649,7 @@ export class etemplate2
|
||||
}
|
||||
else
|
||||
{
|
||||
egw.debug("error", "Could not find target node %s", data.DOMNodeId);
|
||||
egw.debug("error", "Could not find target node %s", data.DOMNodeID);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,13 +17,15 @@ import {et2_createWidget} from "../etemplate/et2_core_widget";
|
||||
import type {IegwAppLocal} from "./egw_global";
|
||||
import Sortable from 'sortablejs/modular/sortable.complete.esm.js';
|
||||
import {et2_valueWidget} from "../etemplate/et2_core_valueWidget";
|
||||
import {nm_action} from "../etemplate/et2_extension_nextmatch_actions";
|
||||
import {fetchAll, nm_action} from "../etemplate/et2_extension_nextmatch_actions";
|
||||
import {Et2Dialog} from "../etemplate/Et2Dialog/Et2Dialog";
|
||||
import {Et2Favorites} from "../etemplate/Et2Favorites/Et2Favorites";
|
||||
import {loadWebComponent} from "../etemplate/Et2Widget/Et2Widget";
|
||||
import {Et2VfsSelectDialog} from "../etemplate/Et2Vfs/Et2VfsSelectDialog";
|
||||
import {Et2Checkbox} from "../etemplate/Et2Checkbox/Et2Checkbox";
|
||||
import type {EgwAction} from "../egw_action/EgwAction";
|
||||
import {Et2MergeDialog} from "../etemplate/Et2Dialog/Et2MergeDialog";
|
||||
import {EgwActionObject} from "../egw_action/EgwActionObject";
|
||||
import type {Et2Details} from "../etemplate/Layout/Et2Details/Et2Details";
|
||||
import {Et2Checkbox} from "../etemplate/Et2Checkbox/Et2Checkbox";
|
||||
import {AcSelect} from "../../../achelper/js/AcSelect/AcSelect";
|
||||
|
||||
/**
|
||||
@ -839,28 +841,90 @@ export abstract class EgwApp
|
||||
let split = _selected[i].id.split("::");
|
||||
ids.push(split[1]);
|
||||
}
|
||||
let document = await this._getMergeDocument(nm?.getInstanceManager(), _action);
|
||||
if(!document.document)
|
||||
let document = await this._getMergeDocument(nm?.getInstanceManager(), _action, _selected);
|
||||
if(!document.documents || document.documents.length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let vars = {
|
||||
..._action.data.merge_data,
|
||||
document: document.document,
|
||||
pdf: document.pdf ?? false,
|
||||
options: document.options,
|
||||
select_all: all,
|
||||
id: ids
|
||||
};
|
||||
if(document.mime == "message/rfc822")
|
||||
if(document.options.link)
|
||||
{
|
||||
vars.options.app = this.appname;
|
||||
}
|
||||
// Just one file, an email - merge & edit or merge & send
|
||||
if(document.documents.length == 1 && document.documents[0].mime == "message/rfc822")
|
||||
{
|
||||
vars.document = document.documents[0].path;
|
||||
return this._mergeEmail(_action.clone(), vars);
|
||||
}
|
||||
else
|
||||
{
|
||||
vars.id = JSON.stringify(ids);
|
||||
vars.document = document.documents.map(f => f.path);
|
||||
}
|
||||
if(document.documents.length == 1 && !document.options.individual)
|
||||
{
|
||||
// Only 1 document, we can open it
|
||||
vars.id = JSON.stringify(ids);
|
||||
this.egw.open_link(this.egw.link('/index.php', vars), '_blank');
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multiple files, or merging individually - will result in multiple documents that we can't just open
|
||||
vars.menuaction = vars.menuaction.replace("merge_entries", "ajax_merge_multiple");
|
||||
vars.menuaction += "&merge=" + vars.merge;
|
||||
let mergedFiles = [];
|
||||
|
||||
// Check for an email template - all other files will be merged & attached
|
||||
let email = document.documents.find(f => f.mime == "message/rfc822");
|
||||
|
||||
// Can we do this in one, or do we have to split it up for feedback?
|
||||
if(!vars.options.individual && (!email || email && !all && ids.length == 1))
|
||||
{
|
||||
vars.options.open_email = !vars.options.download && typeof email != "undefined";
|
||||
|
||||
// Handle it all on the server in one request
|
||||
this.egw.loading_prompt(vars.menuaction, true);
|
||||
mergedFiles = await this.egw.request(vars.menuaction, [vars.id, vars.document, vars.options]);
|
||||
this.egw.loading_prompt(vars.menuaction, false);
|
||||
|
||||
// One entry, email template selected - we can open that in the compose window
|
||||
if(email)
|
||||
{
|
||||
debugger;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.egw.message(mergedFiles, "success");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Merging documents, merge email, attach to email, send.
|
||||
// Handled like this here so we can give feedback, server could do it all in one request
|
||||
let idGroup = await new Promise<string[]>((resolve) =>
|
||||
{
|
||||
if(all)
|
||||
{
|
||||
fetchAll(ids, nm, idsArr => resolve(vars.options.individual ? idsArr : [idsArr]));
|
||||
}
|
||||
else
|
||||
{
|
||||
resolve(vars.options.individual ? ids : [ids])
|
||||
}
|
||||
});
|
||||
Et2Dialog.long_task(null /*done*/, this.egw.lang("Merging"),
|
||||
email ? this.egw.lang("Merging into %1 and sending", email.path) : this.egw.lang("Merging into %1", vars.document.join(", ")),
|
||||
vars.menuaction,
|
||||
idGroup.map((ids) => {return [Array.isArray(ids) ? ids : [ids], vars.document, vars.options];}), this.egw
|
||||
);
|
||||
}
|
||||
}
|
||||
this.egw.open_link(this.egw.link('/index.php', vars), '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -870,10 +934,9 @@ export abstract class EgwApp
|
||||
* @protected
|
||||
*/
|
||||
|
||||
protected _getMergeDocument(et2?, action? : EgwAction) : Promise<{
|
||||
document : string,
|
||||
pdf : boolean,
|
||||
mime : string
|
||||
protected _getMergeDocument(et2?, action? : EgwAction, selected? : EgwActionObject[]) : Promise<{
|
||||
documents : { path : string; mime : string }[];
|
||||
options : { [p : string] : string | boolean }
|
||||
}>
|
||||
{
|
||||
let path = action?.data?.merge_data?.directory ?? "";
|
||||
@ -886,51 +949,33 @@ export abstract class EgwApp
|
||||
d = "/" + d;
|
||||
}
|
||||
});
|
||||
let fileSelect = <Et2VfsSelectDialog><unknown>loadWebComponent('et2-vfs-select-dialog', {
|
||||
class: "egw_app_merge_document",
|
||||
title: this.egw.lang("Insert in document"),
|
||||
mode: "open",
|
||||
path: path ?? dirs?.pop() ?? "",
|
||||
open: true
|
||||
}, et2.widgetContainer);
|
||||
let fileSelect = <Et2MergeDialog><unknown>loadWebComponent('et2-merge-dialog', {
|
||||
application: this.appname,
|
||||
path: dirs.pop() || ""
|
||||
}, et2?.widgetContainer ?? null);
|
||||
if(!et2)
|
||||
{
|
||||
document.body.append(fileSelect);
|
||||
}
|
||||
let pdf = <Et2Checkbox><unknown>loadWebComponent("et2-checkbox", {
|
||||
slot: "footer",
|
||||
label: "As PDF"
|
||||
}, fileSelect);
|
||||
|
||||
// Disable PDF checkbox for emails
|
||||
fileSelect.addEventListener("et2-select", e =>
|
||||
// Customize dialog
|
||||
fileSelect.updateComplete.then(() =>
|
||||
{
|
||||
let canPDF = true;
|
||||
fileSelect.value.forEach(path =>
|
||||
{
|
||||
if(fileSelect.fileInfo(path).mime == "message/rfc822")
|
||||
{
|
||||
canPDF = false;
|
||||
}
|
||||
});
|
||||
pdf.disabled = !canPDF;
|
||||
});
|
||||
return fileSelect.getComplete().then((values) =>
|
||||
{
|
||||
if(!values[0])
|
||||
{
|
||||
return {document: '', pdf: false, mime: ""};
|
||||
}
|
||||
// Start details open when you have multiple selected
|
||||
// @ts-ignore
|
||||
(<Et2Details>fileSelect.shadowRoot.querySelector('et2-details')).open = selected.length > 1;
|
||||
|
||||
const value = values[1].pop() ?? "";
|
||||
const fileInfo = fileSelect.fileInfo(value) ?? {};
|
||||
fileSelect.remove();
|
||||
return {document: value, pdf: pdf.getValue(), mime: fileInfo.mime ?? ""};
|
||||
// Disable individual when only one entry is selected
|
||||
// @ts-ignore
|
||||
(<Et2Checkbox>fileSelect.shadowRoot.querySelector("et2-details > [id='individual']")).disabled = selected.length == 1;
|
||||
});
|
||||
// Remove when done
|
||||
fileSelect.getComplete().then(() => {fileSelect.remove();});
|
||||
|
||||
return fileSelect.getComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merging into an email
|
||||
* Merge into an email, then open it in compose for a single, send directly for multiple
|
||||
*
|
||||
* @param {object} data
|
||||
* @protected
|
||||
|
@ -533,20 +533,22 @@ egw.extend('json', egw.MODULE_WND_LOCAL, function(_app, _wnd)
|
||||
// The ajax request has completed, get just the data & pass it on
|
||||
if(response && response.response)
|
||||
{
|
||||
let data = [];
|
||||
for(let value of response.response)
|
||||
{
|
||||
if(value.type && value.type === "data" && typeof value.data !== "undefined")
|
||||
{
|
||||
// Data was packed in response
|
||||
return value.data;
|
||||
data.push(value.data);
|
||||
}
|
||||
else if (value && typeof value.type === "undefined" && typeof value.data === "undefined")
|
||||
{
|
||||
// Just raw data
|
||||
return value;
|
||||
data.push(value);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
// Normally only 1 data, but multiple etemplate.exec calls can give multiple
|
||||
return data.length > 1 ? data : data[0];
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
@ -100,10 +100,16 @@ egw.extend('preferences', egw.MODULE_GLOBAL, function()
|
||||
if (_callback === false) return undefined;
|
||||
const request = this.json('EGroupware\\Api\\Framework::ajax_get_preference', [_app], _callback, _context);
|
||||
const promise = request.sendRequest(typeof _callback !== 'undefined', 'GET');
|
||||
if (typeof prefs[_app] === 'undefined') prefs[_app] = {};
|
||||
if (typeof prefs[_app] === 'undefined') prefs[_app] = promise;
|
||||
if (_callback === true) return promise.then(() => this.preference(_name, _app));
|
||||
if (typeof _callback === 'function') return false;
|
||||
}
|
||||
else if (typeof prefs[_app] === 'object' && typeof prefs[_app].then === 'function')
|
||||
{
|
||||
if (_callback === false) return undefined;
|
||||
if (_callback === true) return prefs[_app].then(() => this.preference(_name, _app));
|
||||
if (typeof _callback === 'function') return false;
|
||||
}
|
||||
let ret;
|
||||
if (_name === "*")
|
||||
{
|
||||
|
@ -119,6 +119,7 @@ all addressbooks groupdav de Alle Adressbücher
|
||||
all categories common de Alle Kategorien
|
||||
all days common de Alle Tage
|
||||
all fields common de Alle Felder
|
||||
all files common de Alle Dateien
|
||||
all in one groupdav de Gemeinsam in einem
|
||||
all languages common de Alle Sprachen
|
||||
all operations save the template! common de Alle Operation speichern das Template!
|
||||
@ -443,6 +444,7 @@ document '%1' does not exist or is not readable for you! preferences de Das Doku
|
||||
document properties common de Dokument Eigenschaften
|
||||
document title: common de Titel des Dokuments:
|
||||
documentation common de Dokumentation
|
||||
documents common de Dokumente
|
||||
doesn't matter common de Spielt keine Rolle
|
||||
domain common de Domain
|
||||
domain name for mail-address, eg. "%1" common de Domainname für E-Mail Adresse, z.B. "%1"
|
||||
@ -490,6 +492,7 @@ el salvador common de EL SALVADOR
|
||||
element role title common de Element Rolle Titel
|
||||
email common de E-Mail
|
||||
email-address of the user, eg. "%1" common de E-Mail-Adresse des Benutzers, z.B. "%1"
|
||||
emails common de E-Mails
|
||||
embeded css styles, eg. '.red { background: red; }' (note the '.' before the class-name) or '@import url(...)' (class names are global for the whole page!) common de Eingebettete CSS Stile, zb. '.red { background: red; }' (man beachte den '.' vor der CSS class) oder '@import url(...)' (Angaben gelten für die gesamte Seite!)
|
||||
empty file common de Datei leeren
|
||||
enable javascript onchange submit common de JavaScript absenden bei Änderung (onChange) aktivieren
|
||||
@ -700,6 +703,7 @@ if you use "2-factor-authentication", please enter the code here. common de Wenn
|
||||
image common de Grafik
|
||||
image directory relative to document root (use / !), example: common de Bildverzeichnis entsprechend zur Dokumentroot (benutze / !), Beispiel:
|
||||
image url common de Bild-URL
|
||||
images common de Bilder
|
||||
import common de Import
|
||||
import an etemplate from a xml-file common de Importiert ein eTemplate aus einer XML-Datei
|
||||
import table-definitions from existing db-table common de Importiert die Tabellen-Definition aus einer bestehenden Datenbank-Tabelle
|
||||
@ -811,6 +815,7 @@ link is appended to mail allowing recipients to download or modify up to date ve
|
||||
link is appended to mail allowing recipients to download up to date version of files common de Link wird an die E-Mail angefügt und erlaubt Adressaten den aktuellen Inhalt der an gehangenen Dateien herunter zu laden
|
||||
link target %1 not found! common de Keine Verknüpfung zu %1 gefunden!
|
||||
link title of current record common de Link-Titel des aktuellen Datensatzes
|
||||
link to each entry common de Verknüpfung zum einzelnen Eintrag
|
||||
linkapps common de Verknüpfung Anwendungen
|
||||
linked common de Verknüpft
|
||||
linkentry common de Verknüpfung Eintrag
|
||||
@ -866,6 +871,10 @@ maybe common de Vieleicht
|
||||
mayotte common de MAYOTTE
|
||||
medium common de Mittel
|
||||
menu common de Menü
|
||||
merge common de Zusammenführen
|
||||
merge & send common de Zusammenführen & Versenden
|
||||
merge individually common de einzeln zusammenführen
|
||||
merge options common de Optionen beim Zusammenführen
|
||||
merged document filename preferences de Dateiname des zusammengeführten Dokuments
|
||||
message common de Nachricht
|
||||
message ... common de Nachricht ...
|
||||
@ -1184,6 +1193,7 @@ savant2 version differs from savant2 wrapper. <br/>this version: %1 <br/>savants
|
||||
save common de Speichern
|
||||
save all common de Alle speichern
|
||||
save as common de Speichern unter
|
||||
save as PDF common de Als PDF speichern
|
||||
save as zip common de Als ZIP-Datei speichern
|
||||
save selected columns as default preference for all users. common de Speichert die ausgewählten Spalten als Vorgabe für alle Benutzer.
|
||||
save the changes made and close the window common de Speichert die Änderungen uns schließt das Fenster
|
||||
@ -1264,6 +1274,7 @@ select work email address common de Geschäftl. E-Mail-Adresse auswählen
|
||||
select year common de Jahr auswählen
|
||||
selectbox common de Auswahlbox
|
||||
selection common de Auswahl
|
||||
selection of files can only be done in one folder. %1 files unselected. common de Die Auswahl von Dateien kann nur in einem Ordner erfolgen. %1 Dateien nicht ausgewählt.
|
||||
send common de Senden
|
||||
send succeeded to %1 common de Versand erfolgreich zu %1
|
||||
senegal common de SENEGAL
|
||||
@ -1342,6 +1353,7 @@ spain common de SPANIEN
|
||||
span common de Überspannt
|
||||
span, class common de Span, Class
|
||||
special characters common de Sonderzeichen
|
||||
spreadsheets common de Tabellenkalkulationen
|
||||
sri lanka common de SRI LANKA
|
||||
stack common de Stapel
|
||||
start a new search, cancel this link common de Neue Suche starten, diese Verknüpfung abbrechen
|
||||
@ -1503,6 +1515,7 @@ version-number, should be in the form: major.minor.revision.number (eg. 0.9.13.0
|
||||
vertical alignment of row common de Vertikale Ausrichtung der Zeile
|
||||
vfs upload directory common de Dateimanager Upload-Ordner
|
||||
video tutorials common de Video-Tutorials
|
||||
videos common de Videos
|
||||
viet nam common de VIETNAM
|
||||
view common de Anzeigen
|
||||
view linked %1 entries common de Verknüpfte %1 anzeigen
|
||||
|
@ -119,6 +119,7 @@ all addressbooks groupdav en All addressbooks
|
||||
all categories common en All categories
|
||||
all days common en All days
|
||||
all fields common en All fields
|
||||
all files common en All files
|
||||
all in one groupdav en All in one
|
||||
all languages common en All languages
|
||||
all operations save the template! common en All operations save the template!
|
||||
@ -444,6 +445,7 @@ document '%1' does not exist or is not readable for you! preferences en Document
|
||||
document properties common en Document properties
|
||||
document title: common en Document title:
|
||||
documentation common en Documentation
|
||||
documents common en Documents
|
||||
doesn't matter common en Doesn't matter
|
||||
domain common en Domain
|
||||
domain name for mail-address, eg. "%1" common en Domain name for mail address, eg. "%1"
|
||||
@ -491,6 +493,7 @@ el salvador common en EL SALVADOR
|
||||
element role title common en Element role title
|
||||
email common en Email
|
||||
email-address of the user, eg. "%1" common en Email address of the user, eg. "%1"
|
||||
emails common en Emails
|
||||
embeded css styles, eg. '.red { background: red; }' (note the '.' before the class-name) or '@import url(...)' (class names are global for the whole page!) common en Embedded CSS styles, e.g. '.red { background: red; }' (note the '.' before the class-name) or '@import url(...)' (class names are global for the whole page!)
|
||||
empty file common en Empty file
|
||||
enable javascript onchange submit common en Enable JavaScript onchange submit
|
||||
@ -701,6 +704,7 @@ if you use "2-factor-authentication", please enter the code here. common en If y
|
||||
image common en Image
|
||||
image directory relative to document root (use / !), example: common en Image folder relative to document root (use / !), example:
|
||||
image url common en Image URL
|
||||
images common en Images
|
||||
import common en Import
|
||||
import an etemplate from a xml-file common en Import an eTemplate from a XML-file
|
||||
import table-definitions from existing db-table common en Import table definitions from existing DB table
|
||||
@ -812,6 +816,7 @@ link is appended to mail allowing recipients to download or modify up to date ve
|
||||
link is appended to mail allowing recipients to download up to date version of files common en Link is appended to mail allowing recipients to download up to date version of files
|
||||
link target %1 not found! common en Link target %1 not found!
|
||||
link title of current record common en Link title of current record
|
||||
link to each entry common en Link to each entry
|
||||
linkapps common en Link apps
|
||||
linked common en Linked
|
||||
linkentry common en Link entry
|
||||
@ -867,6 +872,10 @@ maybe common en Maybe
|
||||
mayotte common en MAYOTTE
|
||||
medium common en Medium
|
||||
menu common en Menu
|
||||
merge common en Merge
|
||||
merge & send common en Merge & send
|
||||
merge individually common en Merge individually
|
||||
merge options common en Merge options
|
||||
merged document filename preferences en Merged document filename
|
||||
message common en Message
|
||||
message ... common en Message ...
|
||||
@ -1185,6 +1194,7 @@ savant2 version differs from savant2 wrapper. <br/>this version: %1 <br/>savants
|
||||
save common en Save
|
||||
save all common en Save all
|
||||
save as common en Save as
|
||||
save as PDF common en Save as PDF
|
||||
save as zip common en Save as ZIP
|
||||
save selected columns as default preference for all users. common en Save columns as default preference for all users.
|
||||
save the changes made and close the window common en Save changes and close
|
||||
@ -1265,6 +1275,7 @@ select work email address common en Select work email address
|
||||
select year common en Select year
|
||||
selectbox common en Select box
|
||||
selection common en Selection
|
||||
selection of files can only be done in one folder. %1 files unselected. common en Selection of files can only be done in one folder. %1 files unselected.
|
||||
send common en Send
|
||||
send succeeded to %1 common en Send succeeded to %1
|
||||
senegal common en SENEGAL
|
||||
@ -1343,6 +1354,7 @@ spain common en SPAIN
|
||||
span common en Span
|
||||
span, class common en Span, Class
|
||||
special characters common en special characters
|
||||
spreadsheets common en Spreadsheets
|
||||
sri lanka common en SRI LANKA
|
||||
stack common en Stack
|
||||
start a new search, cancel this link common en Start new search, cancel this link
|
||||
@ -1504,6 +1516,7 @@ version-number, should be in the form: major.minor.revision.number (eg. 0.9.13.0
|
||||
vertical alignment of row common en Vertical alignment of row
|
||||
vfs upload directory common en VFS upload folder
|
||||
video tutorials common en Video Tutorials
|
||||
videos common en Videos
|
||||
viet nam common en VIET NAM
|
||||
view common en View
|
||||
view linked %1 entries common en View linked %1 entries
|
||||
|
@ -370,6 +370,11 @@ class JsBase
|
||||
$target = $value;
|
||||
}
|
||||
}
|
||||
// if we unset fields stored directly in the database, they will NOT be updated :(
|
||||
elseif (!$n)
|
||||
{
|
||||
$target = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
unset($parent[$part]);
|
||||
|
@ -31,7 +31,8 @@ class Merge extends Api\Storage\Merge
|
||||
var $public_functions = array(
|
||||
'download_by_request' => true,
|
||||
'show_replacements' => true,
|
||||
"merge_entries" => true
|
||||
'merge_entries' => true,
|
||||
'ajax_merge_multiple' => true,
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -184,6 +184,11 @@ class DateTime extends \DateTime
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return (string)$this->format(self::DATABASE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like DateTime::add, but additional allow to use a string run through DateInterval::createFromDateString
|
||||
*
|
||||
|
@ -7002,9 +7002,10 @@ class Mail
|
||||
* @param string&|false $_folder (passed by reference) will set the folder used. must be set with a folder, but will hold modifications if
|
||||
* folder is modified. Set to false to not keep the message.
|
||||
* @param string& $importID ID for the imported message, used by attachments to identify them unambiguously
|
||||
* @param string[] $attachments Files attached to the email - the same files for every email
|
||||
* @return mixed array of messages with success and failed messages or exception
|
||||
*/
|
||||
function importMessageToMergeAndSend(Storage\Merge $bo_merge, $document, $SendAndMergeTocontacts, &$_folder, &$importID='')
|
||||
function importMessageToMergeAndSend(Storage\Merge $bo_merge, $document, $SendAndMergeTocontacts, &$_folder, &$importID = '', $attachments = [])
|
||||
{
|
||||
$importfailed = false;
|
||||
$processStats = array('success'=>array(),'failed'=>array());
|
||||
@ -7075,6 +7076,10 @@ class Mail
|
||||
|
||||
$mailObject->addReplyTo(Horde_Idna::encode($activeMailProfile['ident_email']),Mail::generateIdentityString($activeMailProfile,false));
|
||||
}
|
||||
foreach($attachments as $file)
|
||||
{
|
||||
$mailObject->addAttachment($file);
|
||||
}
|
||||
if(count($SendAndMergeTocontacts) > 1)
|
||||
{
|
||||
foreach(Mailer::$type2header as $type => $h)
|
||||
|
@ -224,7 +224,12 @@ class Credentials
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$password = self::decrypt($row);
|
||||
try {
|
||||
$password = self::decrypt($row);
|
||||
}
|
||||
catch (NoSessionPassword $e) {
|
||||
$password = self::UNAVAILABLE;
|
||||
}
|
||||
|
||||
// Remove special x char added to the end for \0 trimming escape.
|
||||
if ($type == self::SMIME && substr($password, -1) === 'x') $password = substr($password, 0, -1);
|
||||
@ -533,7 +538,12 @@ class Credentials
|
||||
if (empty($key))
|
||||
{
|
||||
if ($account_id > 0 && $account_id == $GLOBALS['egw_info']['user']['account_id'] &&
|
||||
($key = Api\Cache::getSession('phpgwapi', 'password')))
|
||||
($key = Api\Cache::getSession('phpgwapi', 'password')) &&
|
||||
// do NOT encrypt password if (optional) SAML or OpenIdConnect auth is enabled
|
||||
!array_filter(array_keys(Api\Config::read('phpgwapi')), static function($name)
|
||||
{
|
||||
return str_ends_with($name, '_discovery');
|
||||
}))
|
||||
{
|
||||
$pw_enc = self::USER_AES;
|
||||
$key = base64_decode($key);
|
||||
@ -687,7 +697,7 @@ class Credentials
|
||||
$session_key = Api\Cache::getSession('phpgwapi', 'password');
|
||||
if (empty($session_key))
|
||||
{
|
||||
throw new Api\Exception\AssertionFailed("No session password available!");
|
||||
throw new NoSessionPassword();
|
||||
}
|
||||
$key = base64_decode($session_key);
|
||||
}
|
||||
@ -921,4 +931,15 @@ class Credentials
|
||||
{
|
||||
return isset($GLOBALS['egw_setup']) ? $GLOBALS['egw_setup']->db : $GLOBALS['egw']->db;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown if session has NO user password stored e.g. SingleSignOn via Saml or OpenIdConnect
|
||||
*/
|
||||
class NoSessionPassword extends Api\Exception\AssertionFailed
|
||||
{
|
||||
public function __construct(?string $msg=null, $code=100, ?\Throwable $previous=null)
|
||||
{
|
||||
parent::__construct($msg ?: "No session password available!", $code, $previous);
|
||||
}
|
||||
}
|
@ -115,6 +115,25 @@ class MimeMagic
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a MIME type to all known file extensions
|
||||
*
|
||||
* If we cannot map the type to any file extension, we return an empty array.
|
||||
*
|
||||
* @param string $_type The MIME type to be mapped to a file extension.
|
||||
* @return array with possible file extensions
|
||||
*/
|
||||
public static function mime2extensions($_type)
|
||||
{
|
||||
$type = strtolower($_type);
|
||||
if (isset(self::$mime_alias_map[$type])) $type = self::$mime_alias_map[$type];
|
||||
|
||||
return array_keys(array_filter(self::$mime_extension_map, static function($mime) use ($type)
|
||||
{
|
||||
return $mime == $type;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix old / aliased mime-types by returning valid/default mime-type
|
||||
*
|
||||
|
@ -15,6 +15,7 @@ namespace EGroupware\Api\Storage;
|
||||
|
||||
use DOMDocument;
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Api\Mail;
|
||||
use EGroupware\Api\Vfs;
|
||||
use EGroupware\Collabora\Conversion;
|
||||
use EGroupware\Stylite;
|
||||
@ -2108,7 +2109,7 @@ abstract class Merge
|
||||
try
|
||||
{
|
||||
$_folder = $this->keep_emails ? '' : FALSE;
|
||||
$msgs = $mail_bo->importMessageToMergeAndSend($this, $content_url, $ids, $_folder);
|
||||
$msgs = $mail_bo->importMessageToMergeAndSend($this, $content_url, $ids, $_folder, $import_id);
|
||||
}
|
||||
catch (Api\Exception\WrongUserinput $e)
|
||||
{
|
||||
@ -2484,12 +2485,17 @@ abstract class Merge
|
||||
*
|
||||
* @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.
|
||||
* @param boolean|null $pdf Convert result to PDF
|
||||
* @param Array options
|
||||
* @param boolean options[individual] Instead of merging all entries into the file, merge each entry into its own file
|
||||
* @param boolean options[pdf] Convert result to PDF
|
||||
* @param boolean options[link] Link generated file to the entry
|
||||
* @param boolean $return Return the path of the generated document instead of opening or downloading
|
||||
* @throws Api\Exception
|
||||
* @throws Api\Exception\AssertionFailed
|
||||
*/
|
||||
public static function merge_entries(array $ids = null, Merge &$document_merge = null, $pdf = null)
|
||||
public static function merge_entries(array $ids = null, Merge &$document_merge = null, $options = [], bool $return = null)
|
||||
{
|
||||
// Setup & get what we need
|
||||
if(is_null($document_merge) && class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'], 'EGroupware\\Api\\Storage\\Merge'))
|
||||
{
|
||||
$document_merge = new $_REQUEST['merge']();
|
||||
@ -2499,10 +2505,22 @@ abstract class Merge
|
||||
$document_merge = new Api\Contacts\Merge();
|
||||
}
|
||||
|
||||
if(($error = $document_merge->check_document($_REQUEST['document'], '')))
|
||||
$documents = (array)$_REQUEST['document'];
|
||||
|
||||
// Check for an email
|
||||
$email = null;
|
||||
foreach($documents as $key => $d)
|
||||
{
|
||||
error_log(__METHOD__ . "({$_REQUEST['document']}) $error");
|
||||
return;
|
||||
if(Vfs::mime_content_type($d) == 'message/rfc822')
|
||||
{
|
||||
$email = $d;
|
||||
unset($documents[$key]);
|
||||
}
|
||||
}
|
||||
if($email)
|
||||
{
|
||||
$mail_bo = Api\Mail::getInstance();
|
||||
$mail_bo->openConnection();
|
||||
}
|
||||
|
||||
if(is_null(($ids)))
|
||||
@ -2513,14 +2531,168 @@ abstract class Merge
|
||||
{
|
||||
$ids = self::get_all_ids($document_merge);
|
||||
}
|
||||
|
||||
if(is_null($pdf))
|
||||
foreach(['pdf', 'individual', 'link', 'download'] as $option)
|
||||
{
|
||||
$pdf = (boolean)$_REQUEST['pdf'];
|
||||
$$option = is_null($options) || empty($options) ? (boolean)$_REQUEST['options'][$option] : (boolean)$options[$option];
|
||||
}
|
||||
$app = (is_null($options) ? $_REQUEST['options']['app'] : $options['app']) ?? $GLOBALS['egw_info']['flags']['currentapp'];
|
||||
|
||||
if(is_null($return))
|
||||
{
|
||||
$return = (boolean)$_REQUEST['return'];
|
||||
}
|
||||
|
||||
$filename = $document_merge->get_filename($_REQUEST['document'], $ids);
|
||||
$result = $document_merge->merge_file($_REQUEST['document'], $ids, $filename, '', $header);
|
||||
$id_group = $individual ? $ids : [$ids];
|
||||
$merged = $attach = [];
|
||||
$target = '';
|
||||
foreach($id_group as $ids)
|
||||
{
|
||||
foreach($documents as $document)
|
||||
{
|
||||
if($document != $email)
|
||||
{
|
||||
// Generate file
|
||||
$target = $document_merge->merge_entries_into_document((array)$ids, $document);
|
||||
}
|
||||
|
||||
// PDF conversion
|
||||
if($pdf)
|
||||
{
|
||||
$converted = $document_merge->pdf_conversion($target);
|
||||
$target = $converted;
|
||||
}
|
||||
$merged[] = $target;
|
||||
$attach[] = Vfs::PREFIX . $target;
|
||||
|
||||
// Move to entry
|
||||
if($link)
|
||||
{
|
||||
foreach((array)$ids as $id)
|
||||
{
|
||||
Api\Link::link($app, $id, Api\Link::VFS_APPNAME, Vfs::PREFIX . $target);
|
||||
}
|
||||
}
|
||||
}
|
||||
// One email per id group
|
||||
if($email && $mail_bo)
|
||||
{
|
||||
// Trick merge into not trying to open in compose
|
||||
$mail_ids = $ids;
|
||||
if(is_string($mail_ids))
|
||||
{
|
||||
$mail_ids = [$mail_ids];
|
||||
}
|
||||
if(count((array)$mail_ids) == 1 && !$open_email)
|
||||
{
|
||||
$mail_ids[] = null;
|
||||
}
|
||||
try
|
||||
{
|
||||
// Special email handling so we can grab it and stick it where we want
|
||||
$mail_folder = $document_merge->keep_emails ? (count($id_group) == 1 ? $mail_bo->getDraftFolder() : '') : FALSE;
|
||||
$mail_id = '';
|
||||
$msgs = $mail_bo->importMessageToMergeAndSend($document_merge, Api\Vfs::PREFIX . $email, $mail_ids, $mail_folder, $mail_id, $attach);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
throw new Api\Exception("Unable to send email", 100, $e);
|
||||
}
|
||||
// Save to VFS so we can link to entry
|
||||
if($link || $download)
|
||||
{
|
||||
// Load message
|
||||
$message = $mail_bo->getMessageRawBody($mail_id, '', $mail_folder);
|
||||
if(!$message)
|
||||
{
|
||||
throw new Api\Exception\AssertionFailed("Unable to read merged email\n" . $mail_folder . "/$mail_id");
|
||||
}
|
||||
|
||||
$filename = $document_merge->get_filename($email, (array)$ids);
|
||||
if(!str_ends_with($filename, pathinfo($email, PATHINFO_EXTENSION)))
|
||||
{
|
||||
$filename .= '.' . pathinfo($email, PATHINFO_EXTENSION);
|
||||
}
|
||||
if($download)
|
||||
{
|
||||
$target = $document_merge->get_save_path($filename);
|
||||
|
||||
// Make sure we won't overwrite something already there
|
||||
$target = Vfs::make_unique($target);
|
||||
file_put_contents(Vfs::PREFIX . $target, $message);
|
||||
$merged[] = $target;
|
||||
}
|
||||
if($link)
|
||||
{
|
||||
foreach((array)$ids as $id)
|
||||
{
|
||||
$target = Api\Link::vfs_path($app, $id, $filename);
|
||||
|
||||
// Make sure we won't overwrite something already there
|
||||
$target = Vfs::make_unique($target);
|
||||
|
||||
file_put_contents(Vfs::PREFIX . $target, $message);
|
||||
if(!$download)
|
||||
{
|
||||
$merged[] = $target;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$attach = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Find out what to do with it - can't handle multiple documents directly
|
||||
if($return || count($merged) > 1)
|
||||
{
|
||||
return $merged;
|
||||
}
|
||||
// Open email in compose?
|
||||
if($email && count($id_group) == 1 && $mail_id && class_exists("mail_ui"))
|
||||
{
|
||||
$mail_uid = \mail_ui::generateRowID($mail_bo->profileID, $mail_folder, $mail_id);
|
||||
$mail_popup = '';
|
||||
$mail_info = Api\Link::edit('mail', $mail_uid, $mail_popup);
|
||||
$mail_info['from'] = 'composefromdraft';
|
||||
Api\Framework::popup(Api\Framework::link("/index.php", $mail_info), '_blank', $mail_popup);
|
||||
return;
|
||||
}
|
||||
// Merge done, present to user
|
||||
if($document_merge->get_editable_mimes()[Vfs::mime_content_type($target)] &&
|
||||
!in_array(Vfs::mime_content_type($target), explode(',', $GLOBALS['egw_info']['user']['preferences']['filemanager']['collab_excluded_mimes'])))
|
||||
{
|
||||
\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, true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given IDs into the given document, saves to VFS, and returns the path
|
||||
*
|
||||
* @param array $ids
|
||||
* @param $pdf
|
||||
* @return string|void
|
||||
* @throws Api\Exception\AssertionFailed
|
||||
* @throws Api\Exception\NotFound
|
||||
* @throws Api\Exception\WrongParameter
|
||||
* @throws Vfs\Exception\ProtectedDirectory
|
||||
*/
|
||||
protected function merge_entries_into_document(array $ids = [], $document)
|
||||
{
|
||||
if(($error = $this->check_document($document, '')))
|
||||
{
|
||||
error_log(__METHOD__ . "({$_REQUEST['document']}) $error");
|
||||
return;
|
||||
}
|
||||
|
||||
$filename = $this->get_filename($document, $ids);
|
||||
$result = $this->merge_file($document, $ids, $filename, '', $header);
|
||||
|
||||
if(!is_file($result) || !is_readable($result))
|
||||
{
|
||||
@ -2528,7 +2700,7 @@ abstract class Merge
|
||||
}
|
||||
// Put it into the vfs using user's preferred directory if writable,
|
||||
// or expected home dir (/home/username) if not
|
||||
$target = $document_merge->get_save_path($filename);
|
||||
$target = $this->get_save_path($filename);
|
||||
|
||||
// Make sure we won't overwrite something already there
|
||||
$target = Vfs::make_unique($target);
|
||||
@ -2536,7 +2708,52 @@ abstract class Merge
|
||||
copy($result, Vfs::PREFIX . $target);
|
||||
unlink($result);
|
||||
|
||||
// Find out what to do with it
|
||||
return $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a file into PDF
|
||||
*
|
||||
* Removes the original file
|
||||
*
|
||||
* @param $path
|
||||
* @return mixed|string Path to converted file
|
||||
* @throws Api\Exception\AssertionFailed
|
||||
* @throws Api\Exception\NotFound
|
||||
* @throws Api\Exception\WrongParameter
|
||||
* @throws Vfs\Exception\ProtectedDirectory
|
||||
*/
|
||||
protected function pdf_conversion($path)
|
||||
{
|
||||
$editable_mimes = $this->get_editable_mimes();
|
||||
if($editable_mimes[Vfs::mime_content_type($path)])
|
||||
{
|
||||
$error = '';
|
||||
$converted_path = '';
|
||||
$convert = new Conversion();
|
||||
$convert->convert($path, $converted_path, 'pdf', $error);
|
||||
|
||||
if($error)
|
||||
{
|
||||
error_log(__METHOD__ . "({$_REQUEST['document']}) $path => $converted_path Error in PDF conversion: $error");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove original
|
||||
Vfs::unlink($path);
|
||||
$path = $converted_path;
|
||||
}
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of editable mime types
|
||||
*
|
||||
* @return array|String[]
|
||||
*/
|
||||
protected function get_editable_mimes()
|
||||
{
|
||||
$editable_mimes = array();
|
||||
try
|
||||
{
|
||||
@ -2554,38 +2771,63 @@ abstract class Merge
|
||||
// ignore failed discovery
|
||||
unset($e);
|
||||
}
|
||||
return $editable_mimes;
|
||||
}
|
||||
|
||||
// PDF conversion
|
||||
if($editable_mimes[Vfs::mime_content_type($target)] && $pdf)
|
||||
|
||||
/**
|
||||
* Merge one or more entries into one or more documents.
|
||||
*
|
||||
* @param array|null $ids
|
||||
* @param \EGroupware\Api\Contacts\Merge|null $document_merge
|
||||
* @param $documents
|
||||
* @param $options
|
||||
* @return string[] location(s) of merged files
|
||||
*/
|
||||
public static function ajax_merge_multiple(array $ids = [], $documents = [], $options = [])
|
||||
{
|
||||
$response = Api\Json\Response::get();
|
||||
$_REQUEST['document'] = $documents;
|
||||
$app = $options['app'] ?? $GLOBALS['egw_info']['flags']['currentapp'];
|
||||
$message = implode(', ', Api\Link::titles($app, $ids)) . ":\n";
|
||||
$return = true;
|
||||
$open_email = $options['open_email'];
|
||||
|
||||
try
|
||||
{
|
||||
$error = '';
|
||||
$converted_path = '';
|
||||
$convert = new Conversion();
|
||||
$convert->convert($target, $converted_path, 'pdf', $error);
|
||||
$merge_result = static::merge_entries($ids, $document_merge, $options, !$open_email);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
$response->error($message . $e->getMessage());
|
||||
}
|
||||
|
||||
if($error)
|
||||
foreach($merge_result as $result)
|
||||
{
|
||||
if(is_string($result))
|
||||
{
|
||||
error_log(__METHOD__ . "({$_REQUEST['document']}) $target => $converted_path Error in PDF conversion: $error");
|
||||
if($options['download'])
|
||||
{
|
||||
$response->apply('egw.open_link', [Vfs::download_url($result, true), '_browser']);
|
||||
}
|
||||
$message .= $result . "\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove original
|
||||
Vfs::unlink($target);
|
||||
$target = $converted_path;
|
||||
if($result['failed'])
|
||||
{
|
||||
$response->error($message . join(", ", $result['failed']));
|
||||
}
|
||||
else
|
||||
{
|
||||
if($result['success'])
|
||||
{
|
||||
$message .= join(", ", $result['success']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if($editable_mimes[Vfs::mime_content_type($target)] &&
|
||||
!in_array(Vfs::mime_content_type($target), explode(',', $GLOBALS['egw_info']['user']['preferences']['filemanager']['collab_excluded_mimes'])))
|
||||
{
|
||||
\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, true));
|
||||
}
|
||||
$response->data($message);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3054,7 +3296,7 @@ abstract class Merge
|
||||
'type' => 'taglist',
|
||||
'label' => 'Merged document filename',
|
||||
'name' => self::PREF_DOCUMENT_FILENAME,
|
||||
'values' => self::DOCUMENT_FILENAME_OPTIONS,
|
||||
'values' => static::DOCUMENT_FILENAME_OPTIONS,
|
||||
'help' => 'Choose the default filename for merged documents.',
|
||||
'xmlrpc' => True,
|
||||
'admin' => False,
|
||||
|
@ -2770,6 +2770,7 @@ table.egwGridView_outer thead tr th.noResize:hover {
|
||||
}
|
||||
|
||||
.long_task .message {
|
||||
white-space: break-spaces;
|
||||
height: inherit;
|
||||
display: list-item;
|
||||
border: none;
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14 18h4v3h-4zm2 4h1l2 7-3 3-3-3 2-7zm8-14a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8zm-4.4863 9.166c1.7e-4 1.2169 0.20298 5.982-1.0059 5.998 0.47857 1.5808 1.2001 3.7568 1.7578 4.7051 4.7904-0.2735 8.7344-0.88476 8.7344-0.88476l-3.25-8.1035s-2.7953-1.1474-6.2363-1.7148zm-7.0273 0.0234c-3.3306 0.57539-6.2363 1.6914-6.2363 1.6914l-3.25 8.1035s3.9208 0.6139 8.7344 0.88671c0.55783-0.94744 1.279-3.1254 1.7578-4.707-1.203-0.016-1.0079-4.7296-1.0059-5.9746z" fill="#62686a"/>
|
||||
<path d="m25.143 9.1428a9.1429 9.1429 0 0 1-9.1429 9.1429 9.1429 9.1429 0 0 1-9.1429-9.1429 9.1429 9.1429 0 0 1 9.1429-9.1428 9.1429 9.1429 0 0 1 9.1429 9.1428zm4.7846 11.909m0.9297 9.7882-3.7148-9.2607s-5.6268-2.3162-11.142-2.3162c-5.1235 0-11.142 2.3162-11.142 2.3162l-3.7148 9.2607s7.3952 1.1601 14.857 1.1601c7.3932 0 14.857-1.1601 14.857-1.1601z" fill="#62686a"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 514 B |
@ -128,7 +128,8 @@ class calendar_bo
|
||||
MCAL_RECUR_WEEKLY => 'Weekly',
|
||||
MCAL_RECUR_MONTHLY_WDAY => 'Monthly (by day)',
|
||||
MCAL_RECUR_MONTHLY_MDAY => 'Monthly (by date)',
|
||||
MCAL_RECUR_YEARLY => 'Yearly'
|
||||
MCAL_RECUR_YEARLY => 'Yearly',
|
||||
MCAL_RECUR_RDATE/*calendar_rrule::PERIOD*/ => 'Explicit dates',
|
||||
);
|
||||
/**
|
||||
* @var array recur_days translates MCAL recur-days to verbose labels
|
||||
@ -946,7 +947,7 @@ class calendar_bo
|
||||
foreach($events as $event)
|
||||
{
|
||||
// PERIOD
|
||||
$is_exception = $event['recur_type'] != calendar_rrule::PERIOD && in_array(Api\DateTime::to($event['start'], true), $exceptions);
|
||||
$is_exception = in_array(Api\DateTime::to($event['start'], true), $exceptions);
|
||||
$start = $this->date2ts($event['start'],true);
|
||||
if ($event['whole_day'])
|
||||
{
|
||||
@ -1024,10 +1025,11 @@ class calendar_bo
|
||||
$event[$ts] = $this->date2usertime((int)$event[$ts],$date_format);
|
||||
}
|
||||
}
|
||||
// same with the recur exceptions
|
||||
if (isset($event['recur_exception']) && is_array($event['recur_exception']))
|
||||
// same with the recur exceptions and rdates
|
||||
foreach(['recur_exception', 'recur_rdates'] as $name)
|
||||
{
|
||||
foreach($event['recur_exception'] as &$date)
|
||||
if (!is_array($event[$name] ?? null)) continue;
|
||||
foreach($event[$name] as &$date)
|
||||
{
|
||||
if ($event['whole_day'] && $date_format != 'server')
|
||||
{
|
||||
@ -1139,7 +1141,6 @@ class calendar_bo
|
||||
* @param mixed $start start-date
|
||||
* @param mixed $end end-date
|
||||
* @param array $events where the repetions get inserted
|
||||
* @param array $recur_exceptions with date (in Ymd) as key (and True as values), seems not to be used anymore
|
||||
*/
|
||||
function insert_all_recurrences($event,$_start,$end,&$events)
|
||||
{
|
||||
@ -1165,20 +1166,15 @@ class calendar_bo
|
||||
new Api\DateTime($event['recur_enddate'], calendar_timezones::DateTimeZone($event['tzid']));
|
||||
|
||||
// unset exceptions, as we need to add them as recurrence too, but marked as exception
|
||||
// (Period needs them though)
|
||||
if($event['recur_type'] != calendar_rrule::PERIOD)
|
||||
{
|
||||
unset($event['recur_exception']);
|
||||
}
|
||||
unset($event['recur_exception']);
|
||||
// loop over all recurrences and insert them, if they are after $start
|
||||
$rrule = calendar_rrule::event2rrule($event, !$event['whole_day'], // true = we operate in usertime, like the rest of calendar_bo
|
||||
// For whole day events, just stay in server time
|
||||
$event['whole_day'] ? Api\DateTime::$server_timezone->getName() : Api\DateTime::$user_timezone->getName()
|
||||
);
|
||||
if($event['recur_type'] == calendar_rrule::PERIOD)
|
||||
{
|
||||
unset($event['recur_exception']);
|
||||
}
|
||||
unset($event['recur_rdates']);
|
||||
$event['recur_type'] = MCAL_RECUR_NONE;
|
||||
|
||||
foreach($rrule as $time)
|
||||
{
|
||||
// $time is in timezone of event, convert it to usertime used here
|
||||
@ -1194,7 +1190,7 @@ class calendar_bo
|
||||
if (($ts = $this->date2ts($time)) < $start-$event_length)
|
||||
{
|
||||
//echo "<p>".$time." --> ignored as $ts < $start-$event_length</p>\n";
|
||||
continue; // to early or original event (returned by interator too)
|
||||
continue; // to early or original event (returned by iterator too)
|
||||
}
|
||||
|
||||
$ts_end = $ts + $event_length;
|
||||
@ -1230,7 +1226,7 @@ class calendar_bo
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one repetion of $event for $date_ymd to the $events array, after adjusting its start- and end-time
|
||||
* Adds one repetition of $event for $date_ymd to the $events array, after adjusting its start- and end-time
|
||||
*
|
||||
* @param array $events array in which the event gets inserted
|
||||
* @param array $event event to insert, it has start- and end-date of the first recurrence, not of $date_ymd
|
||||
|
@ -1404,7 +1404,7 @@ class calendar_boupdate extends calendar_bo
|
||||
$this->check_reset_statuses($event, $old_event);
|
||||
|
||||
// set recur-enddate/range-end to real end-date of last recurrence
|
||||
if (!empty($event['recur_type']) && $event['recur_enddate'] && $event['start'])
|
||||
if (!empty($event['recur_type']) && (!empty($event['recur_enddate']) || $event['recur_type'] == calendar_rrule::PERIOD) && $event['start'])
|
||||
{
|
||||
$event['recur_enddate'] = new Api\DateTime($event['recur_enddate'], calendar_timezones::DateTimeZone($event['tzid']));
|
||||
$event['recur_enddate']->setTime(23,59,59);
|
||||
@ -1417,8 +1417,11 @@ class calendar_boupdate extends calendar_bo
|
||||
$occurrence = $rrule->current();
|
||||
}
|
||||
while ($rrule->validDate($event['whole_day']) && ($enddate = $occurrence));
|
||||
$enddate->modify(($event['end'] - $event['start']).' second');
|
||||
$event['recur_enddate'] = $save_event['recur_enddate'] = $enddate->format('ts');
|
||||
if ($enddate)
|
||||
{
|
||||
$enddate->modify(($event['end'] - $event['start']).' second');
|
||||
$event['recur_enddate'] = $enddate->format('ts');
|
||||
}
|
||||
//error_log(__METHOD__."($event[title]) start=".Api\DateTime::to($event['start'],'string').', end='.Api\DateTime::to($event['end'],'string').', range_end='.Api\DateTime::to($event['recur_enddate'],'string'));
|
||||
}
|
||||
|
||||
@ -1485,10 +1488,11 @@ class calendar_boupdate extends calendar_bo
|
||||
{
|
||||
$event['tz_id'] = calendar_timezones::tz2id($event['tzid'] = Api\DateTime::$user_timezone->getName());
|
||||
}
|
||||
// same with the recur exceptions
|
||||
if (isset($event['recur_exception']) && is_array($event['recur_exception']))
|
||||
// same with the recur exceptions and rdates
|
||||
foreach(['recur_exception', 'recur_rdates'] as $name)
|
||||
{
|
||||
foreach($event['recur_exception'] as &$date)
|
||||
if (!is_array($event[$name] ?? null)) continue;
|
||||
foreach($event[$name] as &$date)
|
||||
{
|
||||
if ($event['whole_day'])
|
||||
{
|
||||
@ -2400,9 +2404,9 @@ class calendar_boupdate extends calendar_bo
|
||||
*
|
||||
* @param array $event the vCalendar data we try to find
|
||||
* @param string filter='exact' exact -> find the matching entry
|
||||
* check -> check (consitency) for identical matches
|
||||
* check -> check (consistency) for identical matches
|
||||
* relax -> be more tolerant
|
||||
* master -> try to find a releated series master
|
||||
* master -> try to find a related series master
|
||||
* @return array calendar_ids of matching entries
|
||||
*/
|
||||
function find_event($event, $filter='exact')
|
||||
@ -3120,12 +3124,12 @@ class calendar_boupdate extends calendar_bo
|
||||
// we convert here from server-time to timestamps in user-time!
|
||||
if (isset($event[$ts])) $event[$ts] = $event[$ts] ? $this->date2usertime($event[$ts]) : 0;
|
||||
}
|
||||
// same with the recur exceptions
|
||||
if (isset($event['recur_exception']) && is_array($event['recur_exception']))
|
||||
// same with the recur exceptions and rdates
|
||||
foreach(['recur_exception', 'recur_rdates'] as $name)
|
||||
{
|
||||
foreach($event['recur_exception'] as $n => $date)
|
||||
foreach($event[$name] ?? [] as $n => $date)
|
||||
{
|
||||
$event['recur_exception'][$n] = $this->date2usertime($date);
|
||||
$event[$name][$n] = $this->date2usertime($date);
|
||||
}
|
||||
}
|
||||
// same with the alarms
|
||||
@ -3137,6 +3141,7 @@ class calendar_boupdate extends calendar_bo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete events that are more than $age years old
|
||||
*
|
||||
|
@ -1303,40 +1303,42 @@ class calendar_ical extends calendar_boupdate
|
||||
{
|
||||
$event['id'] = $event_info['stored_event']['id']; // CalDAV does only provide UIDs
|
||||
}
|
||||
if (is_array($event['participants']))
|
||||
// if file contains no participants add current user
|
||||
if (empty($event['participants']))
|
||||
{
|
||||
// if the client does not return a status, we restore the original one
|
||||
foreach ($event['participants'] as $uid => $status)
|
||||
$event['participants'] = [$user => calendar_so::combine_status('A')];
|
||||
}
|
||||
// if the client does not return a status, we restore the original one
|
||||
foreach ($event['participants'] as $uid => $status)
|
||||
{
|
||||
// Work around problems with Outlook CalDAV Synchronizer (https://caldavsynchronizer.org/)
|
||||
// - always sends all participants back with status NEEDS-ACTION --> resets status of all participant, if user has edit rights
|
||||
// --> allow only updates with other status then NEEDS-ACTION and therefore allow accepting or denying meeting requests for the user himself
|
||||
if ($status[0] === 'X' || calendar_groupdav::get_agent() === 'caldavsynchronizer' && $status[0] === 'U')
|
||||
{
|
||||
// Work around problems with Outlook CalDAV Synchronizer (https://caldavsynchronizer.org/)
|
||||
// - always sends all participants back with status NEEDS-ACTION --> resets status of all participant, if user has edit rights
|
||||
// --> allow only updates with other status then NEEDS-ACTION and therefore allow accepting or denying meeting requests for the user himself
|
||||
if ($status[0] === 'X' || calendar_groupdav::get_agent() === 'caldavsynchronizer' && $status[0] === 'U')
|
||||
if (isset($event_info['stored_event']['participants'][$uid]))
|
||||
{
|
||||
if (isset($event_info['stored_event']['participants'][$uid]))
|
||||
if ($this->log)
|
||||
{
|
||||
if ($this->log)
|
||||
{
|
||||
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
|
||||
"() Restore status for $uid\n",3,$this->logfile);
|
||||
}
|
||||
$event['participants'][$uid] = $event_info['stored_event']['participants'][$uid];
|
||||
}
|
||||
else
|
||||
{
|
||||
$event['participants'][$uid] = calendar_so::combine_status('U');
|
||||
error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
|
||||
"() Restore status for $uid\n",3,$this->logfile);
|
||||
}
|
||||
$event['participants'][$uid] = $event_info['stored_event']['participants'][$uid];
|
||||
}
|
||||
// restore resource-quantity from existing event as neither iOS nor Thunderbird returns our X-EGROUPWARE-QUANTITY
|
||||
elseif ($uid[0] === 'r' && isset($event_info['stored_event']['participants'][$uid]))
|
||||
else
|
||||
{
|
||||
$quantity = $role = $old_quantity = null;
|
||||
calendar_so::split_status($status, $quantity, $role);
|
||||
calendar_so::split_status($event_info['stored_event']['participants'][$uid], $old_quantity);
|
||||
if ($old_quantity > 1)
|
||||
{
|
||||
$event['participants'][$uid] = calendar_so::combine_status('U', $old_quantity, $role);
|
||||
}
|
||||
$event['participants'][$uid] = calendar_so::combine_status('U');
|
||||
}
|
||||
}
|
||||
// restore resource-quantity from existing event as neither iOS nor Thunderbird returns our X-EGROUPWARE-QUANTITY
|
||||
elseif ($uid[0] === 'r' && isset($event_info['stored_event']['participants'][$uid]))
|
||||
{
|
||||
$quantity = $role = $old_quantity = null;
|
||||
calendar_so::split_status($status, $quantity, $role);
|
||||
calendar_so::split_status($event_info['stored_event']['participants'][$uid], $old_quantity);
|
||||
if ($old_quantity > 1)
|
||||
{
|
||||
$event['participants'][$uid] = calendar_so::combine_status('U', $old_quantity, $role);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1673,7 +1675,7 @@ class calendar_ical extends calendar_boupdate
|
||||
unset($event['id']);
|
||||
unset($event_info['stored_event']);
|
||||
$event['recur_type'] = MCAL_RECUR_NONE;
|
||||
if (empty($event['recurrence']))
|
||||
if (empty($event['recurrence']) && $event_info['master_event'])
|
||||
{
|
||||
// find an existing exception slot
|
||||
$occurence = $exception = false;
|
||||
@ -2765,10 +2767,10 @@ class calendar_ical extends calendar_boupdate
|
||||
if($attributes['params']['VALUE'] == 'PERIOD')
|
||||
{
|
||||
$vcardData['recur_type'] = calendar_rrule::PERIOD;
|
||||
$vcardData['recur_exception'] = [];
|
||||
$vcardData['recur_rdates'] = [];
|
||||
foreach($attributes['values'] as $date)
|
||||
{
|
||||
$vcardData['recur_exception'][] = mktime(
|
||||
$vcardData['recur_rdates'][] = mktime(
|
||||
$hour,
|
||||
$minutes,
|
||||
$seconds,
|
||||
@ -3185,7 +3187,7 @@ class calendar_ical extends calendar_boupdate
|
||||
$event['priority'] = 2; // default
|
||||
$event['alarm'] = $alarms;
|
||||
|
||||
// now that we know what the vard provides,
|
||||
// now that we know what the ical provides,
|
||||
// we merge that data with the information we have about the device
|
||||
while (($fieldName = array_shift($supportedFields)))
|
||||
{
|
||||
@ -3196,6 +3198,7 @@ class calendar_ical extends calendar_boupdate
|
||||
case 'recur_data':
|
||||
case 'recur_exception':
|
||||
case 'recur_count':
|
||||
case 'recur_rdates':
|
||||
case 'whole_day':
|
||||
// not handled here
|
||||
break;
|
||||
@ -3205,7 +3208,7 @@ class calendar_ical extends calendar_boupdate
|
||||
if ($event['recur_type'] != MCAL_RECUR_NONE)
|
||||
{
|
||||
$event['reference'] = 0;
|
||||
foreach (array('recur_interval','recur_enddate','recur_data','recur_exception','recur_count') as $r)
|
||||
foreach (array('recur_interval','recur_enddate','recur_data','recur_exception','recur_count','recur_rdates') as $r)
|
||||
{
|
||||
if (isset($vcardData[$r]))
|
||||
{
|
||||
|
@ -13,6 +13,7 @@
|
||||
*/
|
||||
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Api\Storage\Merge;
|
||||
|
||||
/**
|
||||
* Calendar - document merge object
|
||||
@ -129,7 +130,21 @@ class calendar_merge extends Api\Storage\Merge
|
||||
return parent::merge_string($content, $ids, $err, $mimetype, $fix, $charset);
|
||||
}
|
||||
|
||||
public static function merge_entries(array $ids = null, \EGroupware\Api\Storage\Merge &$document_merge = null, $pdf = null)
|
||||
/**
|
||||
* 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.
|
||||
* @param Array options
|
||||
* @param boolean options[individual] Instead of merging all entries into the file, merge each entry into its own file
|
||||
* @param boolean options[pdf] Convert result to PDF
|
||||
* @param boolean options[link] Link generated file to the entry
|
||||
* @param boolean $return Return the path of the generated document instead of opening or downloading
|
||||
* @throws Api\Exception
|
||||
* @throws Api\Exception\AssertionFailed
|
||||
*/
|
||||
public static function merge_entries(array $ids = null, Merge &$document_merge = null, $options = [], bool $return = null)
|
||||
{
|
||||
$document_merge = new calendar_merge();
|
||||
|
||||
@ -138,72 +153,75 @@ class calendar_merge extends Api\Storage\Merge
|
||||
$ids = json_decode($_REQUEST['id'], true);
|
||||
}
|
||||
|
||||
// Try to make time span into appropriate ranges to match
|
||||
$template = $ids['view'] ?: '';
|
||||
if(stripos($_REQUEST['document'], 'month') !== false || stripos($_REQUEST['document'], lang('month')) !== false)
|
||||
foreach($_REQUEST['document'] as &$document)
|
||||
{
|
||||
$template = 'month';
|
||||
}
|
||||
if(stripos($_REQUEST['document'], 'week') !== false || stripos($_REQUEST['document'], lang('week')) !== false)
|
||||
{
|
||||
$template = 'week';
|
||||
}
|
||||
// Try to make time span into appropriate ranges to match
|
||||
$template = $ids['view'] ?: '';
|
||||
if(stripos($document, 'month') !== false || stripos($document, lang('month')) !== false)
|
||||
{
|
||||
$template = 'month';
|
||||
}
|
||||
if(stripos($document, 'week') !== false || stripos($document, lang('week')) !== false)
|
||||
{
|
||||
$template = 'week';
|
||||
}
|
||||
|
||||
//error_log("Detected template $template");
|
||||
$date = $ids['date'];
|
||||
$first = $ids['first'];
|
||||
$last = $ids['last'];
|
||||
//error_log("Detected template $template");
|
||||
$date = $ids['date'];
|
||||
$first = $ids['first'];
|
||||
$last = $ids['last'];
|
||||
|
||||
// Pull dates from session if they're not in the request
|
||||
if(!array_key_exists('first', $ids))
|
||||
{
|
||||
$ui = new calendar_ui();
|
||||
$date = $ui->date;
|
||||
$first = $ui->first;
|
||||
$last = $ui->last;
|
||||
}
|
||||
switch($template)
|
||||
{
|
||||
case 'month':
|
||||
// Trim to _only_ the month, do not pad to week start / end
|
||||
$time = new Api\DateTime($date);
|
||||
$timespan = array(array(
|
||||
'start' => Api\DateTime::to($time->format('Y-m-01 00:00:00'), 'ts'),
|
||||
'end' => Api\DateTime::to($time->format('Y-m-t 23:59:59'), 'ts')
|
||||
));
|
||||
break;
|
||||
case 'week':
|
||||
$timespan = array();
|
||||
$start = new Api\DateTime($first);
|
||||
$end = new Api\DateTime($last);
|
||||
$t = clone $start;
|
||||
$t->modify('+1 week')->modify('-1 second');
|
||||
if($t < $end)
|
||||
{
|
||||
do
|
||||
{
|
||||
$timespan[] = array(
|
||||
'start' => $start->format('ts'),
|
||||
'end' => $t->format('ts')
|
||||
);
|
||||
$start->modify('+1 week');
|
||||
$t->modify('+1 week');
|
||||
}
|
||||
while($start < $end);
|
||||
// Pull dates from session if they're not in the request
|
||||
if(!array_key_exists('first', $ids))
|
||||
{
|
||||
$ui = new calendar_ui();
|
||||
$date = $ui->date;
|
||||
$first = $ui->first;
|
||||
$last = $ui->last;
|
||||
}
|
||||
switch($template)
|
||||
{
|
||||
case 'month':
|
||||
// Trim to _only_ the month, do not pad to week start / end
|
||||
$time = new Api\DateTime($date);
|
||||
$timespan = array(array(
|
||||
'start' => Api\DateTime::to($time->format('Y-m-01 00:00:00'), 'ts'),
|
||||
'end' => Api\DateTime::to($time->format('Y-m-t 23:59:59'), 'ts')
|
||||
));
|
||||
break;
|
||||
}
|
||||
// Fall through
|
||||
default:
|
||||
$timespan = array(array(
|
||||
'start' => $first,
|
||||
'end' => $last
|
||||
));
|
||||
case 'week':
|
||||
$timespan = array();
|
||||
$start = new Api\DateTime($first);
|
||||
$end = new Api\DateTime($last);
|
||||
$t = clone $start;
|
||||
$t->modify('+1 week')->modify('-1 second');
|
||||
if($t < $end)
|
||||
{
|
||||
do
|
||||
{
|
||||
$timespan[] = array(
|
||||
'start' => $start->format('ts'),
|
||||
'end' => $t->format('ts')
|
||||
);
|
||||
$start->modify('+1 week');
|
||||
$t->modify('+1 week');
|
||||
}
|
||||
while($start < $end);
|
||||
break;
|
||||
}
|
||||
// Fall through
|
||||
default:
|
||||
$timespan = array(array(
|
||||
'start' => $first,
|
||||
'end' => $last
|
||||
));
|
||||
}
|
||||
|
||||
// Add path into document
|
||||
static::check_document($document, $GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir']);
|
||||
}
|
||||
|
||||
// Add path into document
|
||||
static::check_document($_REQUEST['document'], $GLOBALS['egw_info']['user']['preferences']['calendar']['document_dir']);
|
||||
|
||||
return \EGroupware\Api\Storage\Merge::merge_entries(array_key_exists('0', $ids) ? $ids : $timespan, $document_merge);
|
||||
return parent::merge_entries(array_key_exists('0', $ids) ? $ids : $timespan, $document_merge, $options, $return);
|
||||
}
|
||||
|
||||
public function get_filename_placeholders($document, $ids)
|
||||
@ -1238,4 +1256,4 @@ class calendar_merge extends Api\Storage\Merge
|
||||
}
|
||||
return $placeholders;
|
||||
}
|
||||
}
|
||||
}
|
@ -45,29 +45,28 @@ class calendar_rrule implements Iterator
|
||||
*/
|
||||
const WEEKLY = 2;
|
||||
/**
|
||||
* Monthly recurrance iCal: monthly_bymonthday
|
||||
* Monthly recurrence iCal: monthly_bymonthday
|
||||
*/
|
||||
const MONTHLY_MDAY = 3;
|
||||
/**
|
||||
* Monthly recurrance iCal: BYDAY (by weekday, eg. 1st Friday of month)
|
||||
* Monthly recurrence iCal: BYDAY (by weekday, eg. 1st Friday of month)
|
||||
*/
|
||||
const MONTHLY_WDAY = 4;
|
||||
/**
|
||||
* Yearly recurrance
|
||||
* Yearly recurrence
|
||||
*/
|
||||
const YEARLY = 5;
|
||||
/**
|
||||
* Hourly recurrance
|
||||
* Hourly recurrence
|
||||
*/
|
||||
const HOURLY = 8;
|
||||
/**
|
||||
* Minutely recurrance
|
||||
* Minutely recurrence
|
||||
*/
|
||||
const MINUTELY = 7;
|
||||
|
||||
/**
|
||||
* By date or period
|
||||
* (a list of dates)
|
||||
* RDATE: date or period (a list of dates, instead of a RRULE)
|
||||
*/
|
||||
const PERIOD = 9;
|
||||
|
||||
@ -226,9 +225,9 @@ class calendar_rrule implements Iterator
|
||||
public $time;
|
||||
|
||||
/**
|
||||
* Current "position" / time
|
||||
* Current "position" / time or null, if invalid (out of explicit RDATEs)
|
||||
*
|
||||
* @var Api\DateTime
|
||||
* @var Api\DateTime|null
|
||||
*/
|
||||
public $current;
|
||||
|
||||
@ -257,9 +256,10 @@ class calendar_rrule implements Iterator
|
||||
* @param int $interval =1 1, 2, ...
|
||||
* @param DateTime $enddate =null enddate or null for no enddate (in which case we user '+5 year' on $time)
|
||||
* @param int $weekdays =0 self::SUNDAY=1|self::MONDAY=2|...|self::SATURDAY=64
|
||||
* @param array $exceptions =null DateTime objects with exceptions
|
||||
* @param DateTime[] $exceptions =null DateTime objects with exceptions
|
||||
* @param DateTime[] $rdates =null DateTime objects with rdates (for type self::PERIOD)
|
||||
*/
|
||||
public function __construct(DateTime $time,$type,$interval=1,DateTime $enddate=null,$weekdays=0,array $exceptions=null)
|
||||
public function __construct(DateTime $time,$type,$interval=1,DateTime $enddate=null,$weekdays=0,array $exceptions=null,array $rdates=null)
|
||||
{
|
||||
switch($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts'])
|
||||
{
|
||||
@ -319,16 +319,15 @@ class calendar_rrule implements Iterator
|
||||
$this->enddate = $enddate;
|
||||
if($type == self::PERIOD)
|
||||
{
|
||||
foreach($exceptions as $exception)
|
||||
foreach($rdates as $rdate)
|
||||
{
|
||||
$exception->setTimezone($this->time->getTimezone());
|
||||
$this->period[] = $exception;
|
||||
$rdate->setTimezone($this->time->getTimezone());
|
||||
$this->period[] = $rdate;
|
||||
}
|
||||
$enddate = clone(count($this->period) ? end($this->period) : $this->time);
|
||||
// Make sure to include the last date as valid
|
||||
$enddate->modify('+1 second');
|
||||
reset($this->period);
|
||||
unset($exceptions);
|
||||
}
|
||||
// no recurrence --> current date is enddate
|
||||
if ($type == self::NONE)
|
||||
@ -376,6 +375,7 @@ class calendar_rrule implements Iterator
|
||||
{
|
||||
switch($type)
|
||||
{
|
||||
default:
|
||||
case self::DAILY:
|
||||
$duration = 24*3600;
|
||||
break;
|
||||
@ -413,11 +413,11 @@ class calendar_rrule implements Iterator
|
||||
/**
|
||||
* Return the current element
|
||||
*
|
||||
* @return Api\DateTime
|
||||
* @return ?Api\DateTime
|
||||
*/
|
||||
public function current(): Api\DateTime
|
||||
public function current(): ?Api\DateTime
|
||||
{
|
||||
return clone $this->current;
|
||||
return $this->current ? clone $this->current : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -427,7 +427,7 @@ class calendar_rrule implements Iterator
|
||||
*/
|
||||
public function key(): int
|
||||
{
|
||||
return (int)$this->current->format('Ymd');
|
||||
return $this->current ? (int)$this->current->format('Ymd') : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -498,14 +498,15 @@ class calendar_rrule implements Iterator
|
||||
$this->current->modify($this->interval.' minute');
|
||||
break;
|
||||
case self::PERIOD:
|
||||
$index = array_search($this->current, $this->period);
|
||||
$next = $this->enddate ?? new Api\DateTime();
|
||||
if($index !== false && $index + 1 < count($this->period))
|
||||
if (($next = next($this->period)))
|
||||
{
|
||||
$next = $this->period[$index + 1];
|
||||
$this->current->setDate($next->format('Y'), $next->format('m'), $next->format('d'));
|
||||
$this->current->setTime($next->format('H'), $next->format('i'), $next->format('s'), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->current = null;
|
||||
}
|
||||
$this->current->setDate($next->format('Y'), $next->format('m'), $next->format('d'));
|
||||
$this->current->setTime($next->format('H'), $next->format('i'), $next->format('s'), 0);
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -522,7 +523,7 @@ class calendar_rrule implements Iterator
|
||||
{
|
||||
$this->next_no_exception();
|
||||
}
|
||||
while($this->exceptions && in_array($this->current->format('Ymd'),$this->exceptions));
|
||||
while($this->current && $this->exceptions && in_array($this->current->format('Ymd'),$this->exceptions));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -595,7 +596,14 @@ class calendar_rrule implements Iterator
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->current = clone $this->time;
|
||||
if ($this->type == self::PERIOD)
|
||||
{
|
||||
$this->current = $this->period ? clone reset($this->period) : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->current = clone $this->time;
|
||||
}
|
||||
while ($this->valid() &&
|
||||
$this->exceptions &&
|
||||
in_array($this->current->format('Ymd'),$this->exceptions))
|
||||
@ -612,6 +620,10 @@ class calendar_rrule implements Iterator
|
||||
*/
|
||||
public function validDate(bool $use_just_date=null): bool
|
||||
{
|
||||
if (!$this->current)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if ($use_just_date)
|
||||
{
|
||||
return $this->current->format('Ymd') <= $this->enddate_ymd;
|
||||
@ -626,7 +638,7 @@ class calendar_rrule implements Iterator
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->current->format('ts') < $this->enddate_ts;
|
||||
return $this->current && $this->current->format('ts') < $this->enddate_ts;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -853,10 +865,17 @@ class calendar_rrule implements Iterator
|
||||
{
|
||||
foreach($event['recur_exception'] as $exception)
|
||||
{
|
||||
$exceptions[] = is_a($exception,'DateTime') ? $exception : new Api\DateTime($exception,$timestamp_tz);
|
||||
$exceptions[] = is_a($exception,'DateTime') ? $exception : new Api\DateTime($exception, $timestamp_tz);
|
||||
}
|
||||
}
|
||||
return new calendar_rrule($time,$event['recur_type'],$event['recur_interval'],$enddate??null,$event['recur_data'],$exceptions??null);
|
||||
if (is_array($event['recur_rdates']))
|
||||
{
|
||||
foreach($event['recur_rdates'] as $rdate)
|
||||
{
|
||||
$rdates[] = is_a($rdate,'DateTime') ? $rdate : new Api\DateTime($rdate, $timestamp_tz);
|
||||
}
|
||||
}
|
||||
return new calendar_rrule($time,$event['recur_type'],$event['recur_interval'],$enddate??null,$event['recur_data'],$exceptions??null,$rdates??null);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -955,6 +974,7 @@ class calendar_rrule implements Iterator
|
||||
'recur_enddate' => $this->enddate ? $this->enddate->format('ts') : null,
|
||||
'recur_data' => $this->weekdays,
|
||||
'recur_exception' => $this->exceptions,
|
||||
'recur_rdates' => $this->period,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -43,6 +43,7 @@ if(!extension_loaded('mcal'))
|
||||
define('MCAL_M_WEEKEND',65);
|
||||
define('MCAL_M_ALLDAYS',127);
|
||||
}
|
||||
define('MCAL_RECUR_RDATE',9);
|
||||
|
||||
define('REJECTED',0);
|
||||
define('NO_RESPONSE',1);
|
||||
@ -311,7 +312,7 @@ class calendar_so
|
||||
* All times (start, end and modified) are returned as timesstamps in servertime!
|
||||
*
|
||||
* @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid
|
||||
* @param int $recur_date =0 if set read the next recurrence at or after the timestamp, default 0 = read the initital one
|
||||
* @param int $recur_date =0 if set read the next recurrence at or after the timestamp, default 0 = read the initial one
|
||||
* @param boolean $read_recurrence =false true: read the exception, not the series master (only for recur_date && $ids='<uid>'!)
|
||||
* @return array|boolean array with cal_id => event array pairs or false if entry not found
|
||||
*/
|
||||
@ -426,12 +427,21 @@ class calendar_so
|
||||
}
|
||||
if (!(int)$recur_date && !empty($event['recur_type']))
|
||||
{
|
||||
foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array(
|
||||
foreach($this->db->select($this->dates_table, 'cal_id,cal_start,recur_exception', [
|
||||
'cal_id' => $ids,
|
||||
]+($event['recur_type'] == MCAL_RECUR_RDATE ? [] : [
|
||||
'recur_exception' => true,
|
||||
), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row)
|
||||
]), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row)
|
||||
{
|
||||
$events[$row['cal_id']]['recur_exception'][] = $row['cal_start'];
|
||||
if ($row['recur_exception'])
|
||||
{
|
||||
$events[$row['cal_id']]['recur_exception'][] = $row['cal_start'];
|
||||
}
|
||||
// rdates are both, exceptions and regular dates!
|
||||
if ($event['recur_type'] == MCAL_RECUR_RDATE)
|
||||
{
|
||||
$events[$row['cal_id']]['recur_rdates'][] = $row['cal_start'];
|
||||
}
|
||||
}
|
||||
break; // as above select read all exceptions (and I dont think too short uid problem still exists)
|
||||
}
|
||||
@ -1129,7 +1139,7 @@ class calendar_so
|
||||
{
|
||||
$row['participants'] = array();
|
||||
}
|
||||
$row['recur_exception'] = $row['alarm'] = array();
|
||||
$row['recur_exception'] = $row['recur_rdates'] = $row['alarm'] = array();
|
||||
|
||||
// compile a list of recurrences per cal_id
|
||||
if (!isset($recur_ids[$row['cal_id']]) || !in_array($id, $recur_ids[$row['cal_id']])) $recur_ids[$row['cal_id']][] = $id;
|
||||
@ -1178,9 +1188,8 @@ class calendar_so
|
||||
// query recurrance exceptions, if needed: enum_recuring && !daywise is used in calendar_groupdav::get_series($uid,...)
|
||||
if (!$params['enum_recuring'] || !$params['daywise'])
|
||||
{
|
||||
foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array(
|
||||
foreach($this->db->select($this->dates_table, 'cal_id,cal_start,recur_exception', array(
|
||||
'cal_id' => $ids,
|
||||
'recur_exception' => true,
|
||||
), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row)
|
||||
{
|
||||
// for enum_recurring events are not indexed by cal_id, but $cal_id.'-'.$cal_start
|
||||
@ -1192,7 +1201,14 @@ class calendar_so
|
||||
if ($event['id'] == $row['cal_id']) break;
|
||||
}
|
||||
}
|
||||
$events[$id]['recur_exception'][] = $row['cal_start'];
|
||||
if (Api\Db::from_bool($row['recur_exception']))
|
||||
{
|
||||
$events[$id]['recur_exception'][] = $row['cal_start'];
|
||||
}
|
||||
if ($events[$id]['recur_type'] == MCAL_RECUR_RDATE)
|
||||
{
|
||||
$events[$id]['recur_rdates'][] = $row['cal_start'];
|
||||
}
|
||||
}
|
||||
}
|
||||
//custom fields are not shown in the regular views, so we only query them, if explicitly required
|
||||
@ -1496,7 +1512,7 @@ ORDER BY cal_user_type, cal_usre_id
|
||||
|
||||
if ($cal_id)
|
||||
{
|
||||
// query old recurrance information, before updating main table, where recur_endate is now stored
|
||||
// query old recurrence information, before updating main table, where recur_endate is now stored
|
||||
if (!empty($event['recur_type']))
|
||||
{
|
||||
$old_repeats = $this->db->select($this->repeats_table, "$this->repeats_table.*,range_end AS recur_enddate",
|
||||
@ -1610,7 +1626,7 @@ ORDER BY cal_user_type, cal_usre_id
|
||||
}
|
||||
|
||||
$event['recur_exception'] = is_array($event['recur_exception']) ? $event['recur_exception'] : array();
|
||||
if (!empty($event['recur_exception']))
|
||||
if (count($event['recur_exception']) > 1)
|
||||
{
|
||||
sort($event['recur_exception']);
|
||||
}
|
||||
@ -1707,7 +1723,7 @@ ORDER BY cal_user_type, cal_usre_id
|
||||
// truncate recurrences by given exceptions
|
||||
if (count($event['recur_exception']))
|
||||
{
|
||||
// added and existing exceptions: delete the execeptions from the user table, it could be the first time
|
||||
// added and existing exceptions: delete the exceptions from the user table, it could be the first time
|
||||
$this->db->delete($this->user_table,array('cal_id' => $cal_id,'cal_recur_date' => $event['recur_exception']),__LINE__,__FILE__,'calendar');
|
||||
// update recur_exception flag based on current exceptions
|
||||
$this->db->update($this->dates_table, 'recur_exception='.$this->db->expression($this->dates_table,array(
|
||||
|
@ -113,6 +113,11 @@ class calendar_timezones
|
||||
self::$tz_cache[$id] = Api\Db::strip_array_keys($data,'tz_');
|
||||
}
|
||||
}
|
||||
// check for a Windows timezone without "Standard Time" postfix
|
||||
if (!isset($id) && strpos($tzid, '/') === false)
|
||||
{
|
||||
$id = self::tz2id($tzid.' Standard Time');
|
||||
}
|
||||
if (isset($id) && $what != 'id')
|
||||
{
|
||||
return self::id2tz($id,$what);
|
||||
|
@ -1263,7 +1263,7 @@ export class CalendarApp extends EgwApp
|
||||
}
|
||||
);
|
||||
*/
|
||||
if(typeof framework !== 'undefined' && framework.applications.calendar && framework.applications.calendar.tab)
|
||||
if(typeof framework !== 'undefined' && framework.applications?.calendar && framework.applications.calendar.tab)
|
||||
{
|
||||
let swipe = new tapAndSwipe(framework.applications.calendar.tab.contentDiv, {
|
||||
//Generic swipe handler for all directions
|
||||
@ -2429,7 +2429,7 @@ export class CalendarApp extends EgwApp
|
||||
{
|
||||
// This activates calendar app if you call setState from a different app
|
||||
// such as home. If we change state while not active, sizing is wrong.
|
||||
if(typeof framework !== 'undefined' && framework.applications.calendar && framework.applications.calendar.hasSideboxMenuContent)
|
||||
if(typeof framework !== 'undefined' && framework.applications?.calendar && framework.applications.calendar.hasSideboxMenuContent)
|
||||
{
|
||||
framework.setActiveApp(framework.applications.calendar);
|
||||
}
|
||||
@ -2658,7 +2658,7 @@ export class CalendarApp extends EgwApp
|
||||
// Show loading div to hide redrawing
|
||||
egw.loading_prompt(
|
||||
this.appname,true,egw.lang('please wait...'),
|
||||
typeof framework !== 'undefined' ? framework.applications.calendar.tab.contentDiv : false,
|
||||
typeof framework !== 'undefined' ? framework.applications?.calendar?.tab?.contentDiv : false,
|
||||
egwIsMobile()?'horizontal':'spinner'
|
||||
);
|
||||
|
||||
@ -3483,7 +3483,7 @@ export class CalendarApp extends EgwApp
|
||||
// Show ajax loader
|
||||
if(typeof framework !== 'undefined')
|
||||
{
|
||||
framework.applications.calendar.sidemenuEntry.showAjaxLoader();
|
||||
framework.applications?.calendar?.sidemenuEntry?.showAjaxLoader();
|
||||
}
|
||||
|
||||
if(state.view === 'planner' && state.sortby === 'user')
|
||||
@ -3580,7 +3580,7 @@ export class CalendarApp extends EgwApp
|
||||
// Hide AJAX loader
|
||||
else if(typeof framework !== 'undefined')
|
||||
{
|
||||
framework.applications.calendar.sidemenuEntry.hideAjaxLoader();
|
||||
framework.applications?.calendar?.sidemenuEntry.hideAjaxLoader();
|
||||
egw.loading_prompt('calendar', false)
|
||||
|
||||
}
|
||||
|
@ -247,6 +247,7 @@ exclude weekend calendar de Wochenende ausschließen
|
||||
execute a further action for this entry calendar de Führt einen weiteren Befehl für diesen Eintrag aus
|
||||
existing links calendar de Bestehende Verknüpfungen
|
||||
exists calendar de Existiert
|
||||
explicit dates calendar de Explizite Termine
|
||||
export definition to use for nextmatch export calendar de Export Profil der Listenansicht (Disketten Symbol)
|
||||
exports events from your calendar in ical format. calendar de Exportiert Termine im iCal-Format
|
||||
exports events from your calendar into a csv file. calendar de Exportiert Termine im CSV-Format
|
||||
|
@ -247,6 +247,7 @@ exclude weekend calendar en Exclude Weekend
|
||||
execute a further action for this entry calendar en Execute a further action for this entry
|
||||
existing links calendar en Existing links
|
||||
exists calendar en Exists
|
||||
explicit dates calendar en Explicit dates
|
||||
export definition to use for nextmatch export calendar en Export definition to use for nextmatch export
|
||||
exports events from your calendar in ical format. calendar en Exports events from your calendar in iCal format.
|
||||
exports events from your calendar into a csv file. calendar en Exports events from your calendar into a CSV file.
|
||||
|
@ -120,8 +120,20 @@
|
||||
</row>
|
||||
<row valign="top">
|
||||
<et2-description for="recur_data" value="Repeat days"></et2-description>
|
||||
<et2-select-dow statustext="Days of the week for a weekly repeated event" id="recur_data" rows="6"
|
||||
<et2-vbox>
|
||||
<et2-select-dow statustext="Days of the week for a weekly repeated event" id="recur_data" rows="6"
|
||||
multiple="true" placeholder=""></et2-select-dow>
|
||||
<grid id="recur_rdates" class="recur_rdates">
|
||||
<columns>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<et2-date-time id="$row" readonly="true"></et2-date-time>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</et2-vbox>
|
||||
<et2-vbox>
|
||||
<et2-description value="Exceptions"></et2-description>
|
||||
<et2-button statustext="Create an exception for the given date" label="@exception_label"
|
||||
|
@ -12,7 +12,7 @@ Egroupware
|
||||
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2.0//EN" "https://www.egroupware.org/etemplate2.0.dtd">
|
||||
|
||||
<overlay>
|
||||
<template id="calendar.sidebox">
|
||||
<template id="calendar.sidebox" slot="left">
|
||||
<et2-vbox parentId="calendar-et2_target">
|
||||
<calendar-date id="date"></calendar-date>
|
||||
<et2-textbox type="hidden" id="first"></et2-textbox>
|
||||
|
@ -12,7 +12,7 @@ Egroupware
|
||||
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2.0//EN" "https://www.egroupware.org/etemplate2.0.dtd">
|
||||
|
||||
<overlay>
|
||||
<template id="calendar.todo" width="30%">
|
||||
<template id="calendar.todo" width="30%" slot="right">
|
||||
<et2-box class="calendar_calDayTodos">
|
||||
<et2-box class="calendar_calDayTodosHeader" width="100%">
|
||||
<et2-button align="left" statustext="Add" id="add" image="add" onclick="egw.open('','infolog','add',{action: 'new',type:'task'});"></et2-button>
|
||||
|
@ -12,7 +12,7 @@ Egroupware
|
||||
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2.0//EN" "https://www.egroupware.org/etemplate2.0.dtd">
|
||||
|
||||
<overlay>
|
||||
<template id="calendar.toolbar">
|
||||
<template id="calendar.toolbar" slot="main-header">
|
||||
<et2-button id="add" image="add" class="imageOnly" statustext="add new event" onclick="app.calendar.toolbar_action(widget);" noSubmit="true"></et2-button>
|
||||
<et2-searchbox id="keywords" overlay="false" onchange="app.calendar.update_state({view: 'listview',search: widget.getValue()});return false;" placeholder="Search"></et2-searchbox>
|
||||
<toolbar id="toolbar" width="100%" flat_list="false"/>
|
||||
|
24
calendar/templates/kdots/app.less
Normal file
24
calendar/templates/kdots/app.less
Normal file
@ -0,0 +1,24 @@
|
||||
@import (less) "../default/app.css";
|
||||
|
||||
#calendar-view, #calendar-todo, #calendar-planner, #calendar-list {
|
||||
position: relative;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#calendar-sidebox {
|
||||
width: 100%;
|
||||
display: block !important;
|
||||
iframe {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#calendar-todo {
|
||||
width: 100%;
|
||||
height: calc(100% - 10px);
|
||||
left: initial !important;
|
||||
}
|
||||
|
||||
.calendar_calEvent:not([class*=" cat_"]) {
|
||||
background-color: var(--primary-background-color)
|
||||
}
|
@ -2046,6 +2046,11 @@ div.calendar {
|
||||
width: 100%;
|
||||
line-height: 30px;
|
||||
}
|
||||
#calendar-edit #calendar-edit_calendar-edit-recurrence table.recur_rdates tbody {
|
||||
display: table;
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
}
|
||||
#calendar-edit #calendar-edit_calendar-edit-custom tbody {
|
||||
display: table;
|
||||
width: 100%;
|
||||
|
@ -2034,6 +2034,11 @@ div.calendar {
|
||||
width: 100%;
|
||||
line-height: 30px;
|
||||
}
|
||||
#calendar-edit #calendar-edit_calendar-edit-recurrence table.recur_rdates tbody {
|
||||
display: table;
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
}
|
||||
#calendar-edit #calendar-edit_calendar-edit-custom tbody {
|
||||
display: table;
|
||||
width: 100%;
|
||||
|
@ -489,6 +489,13 @@ div.calendar { position: relative; }
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
#calendar-edit_calendar-edit-recurrence{
|
||||
table.recur_rdates tbody {
|
||||
display: table;
|
||||
width: 100%;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/*###########################################*/
|
||||
// Benutzerdefiniert
|
||||
|
187
doc/REST-CalDAV-CardDAV/api-client.php
Executable file
187
doc/REST-CalDAV-CardDAV/api-client.php
Executable file
@ -0,0 +1,187 @@
|
||||
<?php
|
||||
/**
|
||||
* EGroupware - REST API client for PHP
|
||||
*
|
||||
* @link https://www.egroupware.org
|
||||
* @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @package api
|
||||
* @subpackage caldav/rest
|
||||
* @author Ralf Becker <rb-at-egroupware.org>
|
||||
* @copyright (c) 2024 by Ralf Becker <rb-at-egroupware.org>
|
||||
*/
|
||||
|
||||
/* Example usage of this client:
|
||||
require_once('/path/to/egroupware/doc/api-client.php');
|
||||
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
die('This script can only be run from the command line.');
|
||||
}
|
||||
$base_url = 'https://egw.example.org/egroupware/groupdav.php';
|
||||
$authorization[parse_url($base_url, PHP_URL_HOST)] = 'Authorization: Basic '.base64_encode('sysop:secret');
|
||||
|
||||
$params = [
|
||||
'filters[info_status]' => 'archive',
|
||||
];
|
||||
$courses = [];
|
||||
foreach(apiIterator('/infolog/', $params) as $infolog)
|
||||
{
|
||||
echo json_encode($infolog, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)."\n";
|
||||
foreach($infolog['participants'] as $account_id => $participant)
|
||||
{
|
||||
if ($participant['roles']['owner'] ?? false)
|
||||
{
|
||||
echo json_encode($contact=api('/addressbook-accounts/'.$account_id),JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)."\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Iterate through API calls on collections
|
||||
*
|
||||
* This function only queries a limited number of entries (default 100) and uses sync-token to query more.
|
||||
*
|
||||
* @param string $url either path (starting with / and prepending global $base_url) or full URL
|
||||
* @param array& $params can contain optional "sync-token" (default="") and "nresults" (default=100) and returns final "sync-token"
|
||||
* @return Generator<array> yields array with additional value for key "@self" containing the key of the responses-object yielded
|
||||
* @throws JsonException|Exception see api
|
||||
*/
|
||||
function apiIterator(string $url, array &$params=[])
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
if (!isset($params['nresults']))
|
||||
{
|
||||
$params['nresults'] = 100;
|
||||
}
|
||||
if (!isset($params['sync-token']))
|
||||
{
|
||||
$params['sync-token']='';
|
||||
}
|
||||
$responses = api($url, 'GET', $params);
|
||||
if (!isset($responses['responses']))
|
||||
{
|
||||
throw new \Exception('Invalid respose: '.(is_scalar($responses) ? $responses : json_encode($responses)));
|
||||
}
|
||||
foreach($responses['responses'] as $self => $response)
|
||||
{
|
||||
$response['@self'] = $self;
|
||||
|
||||
yield $response;
|
||||
}
|
||||
$params['sync-token'] = $responses['sync-token'] ?? '';
|
||||
if (empty($responses['more-results']))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API call to given URL
|
||||
*
|
||||
* Authorization is added from global $authorization array indexed by host-name of $url or $base_url
|
||||
*
|
||||
* @param string $url either path (starting with / and prepending global $base_url) or full URL
|
||||
* @param string $method
|
||||
* @param string|array|resource $body for GET&DELETE this is added as query and must not be a resource/file-handle
|
||||
* @param array $header
|
||||
* @param array|null $response_header associative array of response headers, key 0 has HTTP status
|
||||
* @param int $follow how many redirects to follow, default 3, can be set to 0 to NOT follow
|
||||
* @return array|string array of decoded JSON or string body
|
||||
* @throws JsonException for invalid JSON
|
||||
* @throws Exception with code=0: opening http connection, code=HTTP status, if status is NOT 2xx
|
||||
*/
|
||||
function api(string $url, string $method='GET', $body='', array $header=['Content-Type: application/json'], ?array &$response_header=null, int $follow=3)
|
||||
{
|
||||
global $base_url, $authorization;
|
||||
|
||||
if ($url[0] === '/')
|
||||
{
|
||||
$url = $base_url . $url;
|
||||
}
|
||||
if (in_array(strtoupper($method), ['GET', 'DELETE']) && $body && !is_resource($body))
|
||||
{
|
||||
$url .= '?' . (is_array($body) ? http_build_query($body) : $body);
|
||||
}
|
||||
if (!($curl = curl_init($url)))
|
||||
{
|
||||
throw new Exception(curl_error($curl));
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_HEADER, true);
|
||||
if ($follow > 0)
|
||||
{
|
||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($curl, CURLOPT_MAXREDIRS, $follow);
|
||||
}
|
||||
|
||||
switch (strtoupper($method))
|
||||
{
|
||||
case 'POST':
|
||||
curl_setopt($curl, CURLOPT_POST, true);
|
||||
break;
|
||||
case 'PUT':
|
||||
case 'DELETE':
|
||||
case 'PATCH':
|
||||
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, strtoupper($method));
|
||||
break;
|
||||
case 'GET':
|
||||
curl_setopt($curl, CURLOPT_HTTPGET, true);
|
||||
break;
|
||||
}
|
||||
$header = array_merge($header, ['User-Agent: '.basename(__FILE__, '.php'), $authorization[parse_url($url, PHP_URL_HOST)]]);
|
||||
if (in_array(strtoupper($method), ['POST', 'PUT', 'PATCH']))
|
||||
{
|
||||
if (is_resource($body))
|
||||
{
|
||||
fseek($body, 0, SEEK_END);
|
||||
curl_setopt($curl, CURLOPT_INFILESIZE, ftell($body));
|
||||
fseek($body, 0);
|
||||
}
|
||||
curl_setopt($curl, is_resource($body) ? CURLOPT_INFILE : CURLOPT_POSTFIELDS, is_array($body) ? json_encode($body) : $body);
|
||||
}
|
||||
if (!array_filter($header, function($header)
|
||||
{
|
||||
return stripos($header, 'Accept:') === 0;
|
||||
}))
|
||||
{
|
||||
$header[] = 'Accept: application/json';
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
|
||||
$response_header = [];
|
||||
if (($response = curl_exec($curl)) === false)
|
||||
{
|
||||
throw new Exception(curl_error($curl), 0);
|
||||
}
|
||||
do {
|
||||
[$rheader, $response] = explode("\r\n\r\n", $response, 2);
|
||||
foreach (explode("\r\n", $rheader) as $line)
|
||||
{
|
||||
list($key, $value) = explode(':', $line, 2) + [null, null];
|
||||
if (!isset($value))
|
||||
{
|
||||
$response_header[0] = $key;
|
||||
}
|
||||
else
|
||||
{
|
||||
$response_header[strtolower($key)] = trim($value);
|
||||
}
|
||||
}
|
||||
[, $http_status] = explode(' ', $response_header[0], 2);
|
||||
}
|
||||
while ($http_status[0] === '3' && $follow && preg_match('#^HTTP/[\d.]+ \d+#', $response));
|
||||
|
||||
if ($http_status[0] !== '2')
|
||||
{
|
||||
throw new Exception("Unexpected HTTP status code $http_status: $response", (int)$http_status);
|
||||
}
|
||||
if ($response !== '' && preg_match('#^application/([^+; ]+\+)?json(;|$)#', $response_header['content-type']))
|
||||
{
|
||||
return json_decode($response, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
return $response;
|
||||
}
|
@ -109,8 +109,8 @@ services:
|
||||
# old API and eTemplate(1), required for upgrades from before 14.3
|
||||
#- EGW_EXTRA_APP_OLDAPI=https://github.com/EGroupware/phpgwapi.git https://github.com/EGroupware/etemplate.git
|
||||
#
|
||||
# XDEBUG_REMOTE_HOST need to be set, if the host running the IDE is different from 172.17.0.1 (Mac can use docker.for.mac.localhost)
|
||||
- XDEBUG_REMOTE_HOST=172.17.0.1
|
||||
# XDEBUG_REMOTE_HOST need to be set, Docker Desktop can use host.docker.internal or Linux the docker0 interface 172.17.0.1
|
||||
- XDEBUG_REMOTE_HOST=host.docker.internal
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
@ -228,7 +228,14 @@ services:
|
||||
# Rocket.Chat server
|
||||
#rocketchat:
|
||||
# image: quay.io/egroupware/rocket.chat:latest
|
||||
# command: bash -c 'for i in `seq 1 30`; do node main.js && s=$$? && break || s=$$?; echo "Tried $$i times. Waiting 5 secs..."; sleep 5; done; (exit $$s)'
|
||||
# command: >
|
||||
# sh -c
|
||||
# "while true; do
|
||||
# node main.js &&
|
||||
# s=$$? && break || s=$$?;
|
||||
# echo \"Could not reach MongoDB. Waiting 5 secs ...\";
|
||||
# sleep 5;
|
||||
# done; (exit $$s)"
|
||||
# restart: unless-stopped
|
||||
# volumes:
|
||||
# - $PWD/data/default/rocketchat/uploads:/app/uploads
|
||||
|
@ -218,7 +218,14 @@ services:
|
||||
# Rocket.Chat server
|
||||
rocketchat:
|
||||
image: rocketchat/rocket.chat:latest
|
||||
command: bash -c 'for i in `seq 1 30`; do node main.js && s=$$? && break || s=$$?; echo "Tried $$i times. Waiting 5 secs..."; sleep 5; done; (exit $$s)'
|
||||
command: >
|
||||
sh -c
|
||||
"while true; do
|
||||
node main.js &&
|
||||
s=$$? && break || s=$$?;
|
||||
echo \"Could not reach MongoDB. Waiting 5 secs ...\";
|
||||
sleep 5;
|
||||
done; (exit $$s)"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- $PWD/data/default/rocketchat/uploads:/app/uploads
|
||||
|
@ -54,6 +54,7 @@ $overwrites = [
|
||||
'Et2InputWidget' => [
|
||||
'.attrs' => [
|
||||
'tabindex' => 'int',
|
||||
'value' => 'string',
|
||||
],
|
||||
],
|
||||
'Et2Textbox' => [
|
||||
@ -61,8 +62,12 @@ $overwrites = [
|
||||
'placeholder' => 'string',
|
||||
'maxlength' => 'int',
|
||||
'size' => 'int',
|
||||
'type' => 'string',
|
||||
],
|
||||
],
|
||||
'et2-textbox' => [
|
||||
'.children' => ['.quantity' => 'optional', 'et2-image'],
|
||||
],
|
||||
'Et2InvokerMixin' => 'Et2TextBox',
|
||||
'et2-description' => [
|
||||
'.attrs' => [
|
||||
@ -75,6 +80,7 @@ $overwrites = [
|
||||
'rows' => 'int',
|
||||
'resizeRatio' => 'number', // is this correct
|
||||
'size' => 'int',
|
||||
'placeholder' => 'string',
|
||||
],
|
||||
],
|
||||
'et2-date' => [
|
||||
@ -111,6 +117,9 @@ $overwrites = [
|
||||
'et2-tab-panel' => null,
|
||||
'et2-details' => [
|
||||
'.children' => 'Widgets',
|
||||
'.attrs' => [
|
||||
'summary' => 'string',
|
||||
],
|
||||
],
|
||||
'et2-split' => [
|
||||
'.children' => 'Widgets',
|
||||
@ -129,6 +138,7 @@ $overwrites = [
|
||||
'.attrs' => [
|
||||
'image' => 'string',
|
||||
'noSubmit' => 'boolean',
|
||||
'hideOnReadonly' => 'boolean',
|
||||
],
|
||||
],
|
||||
'Et2ButtonIcon' => 'Et2Button', // no inheritance from Et2Button, but Et2ButtonMixin, which is not recognised
|
||||
|
@ -25,11 +25,11 @@
|
||||
|tracker-value|records-value|hidden|radio|radiogroup|diff|styles|customfields|customfields-list|html
|
||||
|htmlarea|toolbar|historylog|hrule|file|progress|vfs|vfs-name|vfs-size|vfs-mode|vfs-upload|video
|
||||
|audio|barcode|itempicker|script|countdown|customfields-types|nextmatch|nextmatch-header
|
||||
|nextmatch-customfields|nextmatch-sortheader|et2-avatar|et2-avatar-group|et2-lavatar|et2-button
|
||||
|et2-button-icon|et2-button-scroll|et2-button-timestamp|et2-checkbox|et2-colorpicker|et2-date
|
||||
|nextmatch-customfields|nextmatch-sortheader|et2-avatar|et2-avatar-group|et2-lavatar|et2-checkbox
|
||||
|et2-button|et2-button-icon|et2-button-scroll|et2-button-timestamp|et2-colorpicker|et2-date
|
||||
|et2-date-duration|et2-date-range|et2-date-since|et2-date-time|et2-date-timeonly|et2-date-time-today
|
||||
|et2-description|et2-label|et2-dialog|et2-dropdown-button|et2-email|et2-favorites|et2-iframe
|
||||
|et2-appicon|et2-image|et2-link|et2-link-add|et2-link-apps|et2-link-entry|et2-link-list
|
||||
|et2-description|et2-label|et2-dialog|et2-merge-dialog|et2-dropdown-button|et2-email|et2-favorites
|
||||
|et2-iframe|et2-appicon|et2-image|et2-link|et2-link-add|et2-link-apps|et2-link-entry|et2-link-list
|
||||
|et2-link-paste-dialog|et2-link-search|et2-link-string|et2-link-to|et2-portlet|et2-listbox
|
||||
|et2-select|et2-spinner|et2-switch|et2-textarea|et2-number|et2-password|et2-searchbox|et2-textbox
|
||||
|et2-tree|et2-tree-dropdown|et2-tree-cat|et2-url|et2-url-email|et2-url-fax|et2-url-phone
|
||||
@ -1385,6 +1385,38 @@
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-checkbox EMPTY>
|
||||
|
||||
<!ATTLIST et2-checkbox
|
||||
selectedValue CDATA 'true'
|
||||
unselectedValue CDATA ''
|
||||
label CDATA #IMPLIED
|
||||
readonly (false|true|1) 'false'
|
||||
required (false|true|1) 'false'
|
||||
onchange CDATA #IMPLIED
|
||||
autofocus (false|true|1) #IMPLIED
|
||||
autocomplete CDATA 'on'
|
||||
ariaLabel CDATA #IMPLIED
|
||||
ariaDescription CDATA #IMPLIED
|
||||
helpText CDATA #IMPLIED
|
||||
class CDATA #IMPLIED
|
||||
disabled (false|true|1) 'false'
|
||||
hidden (false|true|1) #IMPLIED
|
||||
accesskey CDATA #IMPLIED
|
||||
parentId CDATA #IMPLIED
|
||||
statustext CDATA #IMPLIED
|
||||
onclick CDATA #IMPLIED
|
||||
noLang (false|true|1) #IMPLIED
|
||||
align CDATA #IMPLIED
|
||||
data CDATA #IMPLIED
|
||||
id CDATA #IMPLIED
|
||||
width CDATA #IMPLIED
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-button EMPTY>
|
||||
|
||||
<!ATTLIST et2-button
|
||||
@ -1414,7 +1446,9 @@
|
||||
slot CDATA #IMPLIED
|
||||
image CDATA #IMPLIED
|
||||
noSubmit (false|true|1) #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
hideOnReadonly (false|true|1) #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-button-icon EMPTY>
|
||||
|
||||
@ -1445,7 +1479,9 @@
|
||||
slot CDATA #IMPLIED
|
||||
image CDATA #IMPLIED
|
||||
noSubmit (false|true|1) #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
hideOnReadonly (false|true|1) #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-button-scroll EMPTY>
|
||||
|
||||
@ -1456,7 +1492,8 @@
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
image CDATA #IMPLIED
|
||||
noSubmit (false|true|1) #IMPLIED>
|
||||
noSubmit (false|true|1) #IMPLIED
|
||||
hideOnReadonly (false|true|1) #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-button-timestamp EMPTY>
|
||||
|
||||
@ -1490,38 +1527,9 @@
|
||||
slot CDATA #IMPLIED
|
||||
image CDATA #IMPLIED
|
||||
noSubmit (false|true|1) #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-checkbox EMPTY>
|
||||
|
||||
<!ATTLIST et2-checkbox
|
||||
selectedValue CDATA 'true'
|
||||
unselectedValue CDATA ''
|
||||
label CDATA #IMPLIED
|
||||
readonly (false|true|1) 'false'
|
||||
required (false|true|1) 'false'
|
||||
onchange CDATA #IMPLIED
|
||||
autofocus (false|true|1) #IMPLIED
|
||||
autocomplete CDATA 'on'
|
||||
ariaLabel CDATA #IMPLIED
|
||||
ariaDescription CDATA #IMPLIED
|
||||
helpText CDATA #IMPLIED
|
||||
class CDATA #IMPLIED
|
||||
disabled (false|true|1) 'false'
|
||||
hidden (false|true|1) #IMPLIED
|
||||
accesskey CDATA #IMPLIED
|
||||
parentId CDATA #IMPLIED
|
||||
statustext CDATA #IMPLIED
|
||||
onclick CDATA #IMPLIED
|
||||
noLang (false|true|1) #IMPLIED
|
||||
align CDATA #IMPLIED
|
||||
data CDATA #IMPLIED
|
||||
id CDATA #IMPLIED
|
||||
width CDATA #IMPLIED
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
hideOnReadonly (false|true|1) #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-colorpicker EMPTY>
|
||||
|
||||
@ -1550,7 +1558,8 @@
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-date EMPTY>
|
||||
|
||||
@ -1585,7 +1594,8 @@
|
||||
slot CDATA #IMPLIED
|
||||
yearRange CDATA #IMPLIED
|
||||
dataFormat CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-date-duration EMPTY>
|
||||
|
||||
@ -1622,7 +1632,8 @@
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-date-range EMPTY>
|
||||
|
||||
@ -1708,7 +1719,8 @@
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-date-timeonly EMPTY>
|
||||
|
||||
@ -1741,7 +1753,8 @@
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-date-time-today EMPTY>
|
||||
|
||||
@ -1849,6 +1862,28 @@
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-merge-dialog EMPTY>
|
||||
|
||||
<!ATTLIST et2-merge-dialog
|
||||
application CDATA #IMPLIED
|
||||
path CDATA #IMPLIED
|
||||
class CDATA #IMPLIED
|
||||
disabled (false|true|1) 'false'
|
||||
hidden (false|true|1) #IMPLIED
|
||||
accesskey CDATA #IMPLIED
|
||||
parentId CDATA #IMPLIED
|
||||
statustext CDATA #IMPLIED
|
||||
label CDATA #IMPLIED
|
||||
onclick CDATA #IMPLIED
|
||||
noLang (false|true|1) #IMPLIED
|
||||
align CDATA #IMPLIED
|
||||
data CDATA #IMPLIED
|
||||
id CDATA #IMPLIED
|
||||
width CDATA #IMPLIED
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-dropdown-button EMPTY>
|
||||
|
||||
<!ATTLIST et2-dropdown-button
|
||||
@ -1882,7 +1917,7 @@
|
||||
<!ELEMENT et2-email EMPTY>
|
||||
|
||||
<!ATTLIST et2-email
|
||||
value CDATA '[]'
|
||||
value CDATA #IMPLIED
|
||||
placeholder CDATA ''
|
||||
allowDragAndDrop CDATA 'true'
|
||||
allowPlaceholder (false|true|1) #IMPLIED
|
||||
@ -2219,7 +2254,7 @@
|
||||
noLang (false|true|1) #IMPLIED
|
||||
align CDATA #IMPLIED
|
||||
data CDATA #IMPLIED
|
||||
value CDATA '[]'
|
||||
value CDATA #IMPLIED
|
||||
title CDATA 'Select'
|
||||
mode CDATA #IMPLIED
|
||||
buttonLabel CDATA 'Select'
|
||||
@ -2484,7 +2519,8 @@
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-textarea EMPTY>
|
||||
|
||||
@ -2518,7 +2554,9 @@
|
||||
rows CDATA #IMPLIED
|
||||
resizeRatio CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
placeholder CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-number EMPTY>
|
||||
|
||||
@ -2556,7 +2594,9 @@
|
||||
placeholder CDATA #IMPLIED
|
||||
maxlength CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
type CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-password EMPTY>
|
||||
|
||||
@ -2592,7 +2632,9 @@
|
||||
placeholder CDATA #IMPLIED
|
||||
maxlength CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
type CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-searchbox EMPTY>
|
||||
|
||||
@ -2628,9 +2670,11 @@
|
||||
placeholder CDATA #IMPLIED
|
||||
maxlength CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
type CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-textbox EMPTY>
|
||||
<!ELEMENT et2-textbox (et2-image)?>
|
||||
|
||||
<!ATTLIST et2-textbox
|
||||
validator CDATA #IMPLIED
|
||||
@ -2662,7 +2706,9 @@
|
||||
placeholder CDATA #IMPLIED
|
||||
maxlength CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
type CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-tree EMPTY>
|
||||
|
||||
@ -2815,7 +2861,9 @@
|
||||
placeholder CDATA #IMPLIED
|
||||
maxlength CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
type CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-url-email EMPTY>
|
||||
|
||||
@ -2850,7 +2898,9 @@
|
||||
placeholder CDATA #IMPLIED
|
||||
maxlength CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
type CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-url-fax EMPTY>
|
||||
|
||||
@ -2884,7 +2934,9 @@
|
||||
placeholder CDATA #IMPLIED
|
||||
maxlength CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
type CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-url-phone EMPTY>
|
||||
|
||||
@ -2918,7 +2970,9 @@
|
||||
placeholder CDATA #IMPLIED
|
||||
maxlength CDATA #IMPLIED
|
||||
size CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
type CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-vfs-mime EMPTY>
|
||||
|
||||
@ -2983,7 +3037,7 @@
|
||||
|
||||
<!ATTLIST et2-vfs-select
|
||||
image CDATA #IMPLIED
|
||||
value CDATA '[]'
|
||||
value CDATA #IMPLIED
|
||||
title CDATA 'Select'
|
||||
mode CDATA #IMPLIED
|
||||
buttonLabel CDATA 'Select'
|
||||
@ -3022,7 +3076,7 @@
|
||||
<!ELEMENT et2-vfs-select-dialog EMPTY>
|
||||
|
||||
<!ATTLIST et2-vfs-select-dialog
|
||||
value CDATA '[]'
|
||||
value CDATA #IMPLIED
|
||||
title CDATA 'Select'
|
||||
mode CDATA #IMPLIED
|
||||
buttonLabel CDATA 'Select'
|
||||
@ -4174,7 +4228,8 @@
|
||||
width CDATA #IMPLIED
|
||||
height CDATA #IMPLIED
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED>
|
||||
slot CDATA #IMPLIED
|
||||
summary CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-split (%Widgets;)+>
|
||||
|
||||
@ -4341,7 +4396,8 @@
|
||||
span CDATA #IMPLIED
|
||||
slot CDATA #IMPLIED
|
||||
emptyLabel CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED>
|
||||
tabindex CDATA #IMPLIED
|
||||
value CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT et2-nextmatch-header-entry EMPTY>
|
||||
|
||||
@ -4418,4 +4474,4 @@
|
||||
slot CDATA #IMPLIED
|
||||
rows CDATA #IMPLIED
|
||||
tabindex CDATA #IMPLIED
|
||||
allowFreeEntries (false|true|1) #IMPLIED>
|
||||
allowFreeEntries (false|true|1) #IMPLIED>
|
||||
|
@ -66,11 +66,11 @@
|
||||
<ref name="et2-avatar"/>
|
||||
<ref name="et2-avatar-group"/>
|
||||
<ref name="et2-lavatar"/>
|
||||
<ref name="et2-checkbox"/>
|
||||
<ref name="et2-button"/>
|
||||
<ref name="et2-button-icon"/>
|
||||
<ref name="et2-button-scroll"/>
|
||||
<ref name="et2-button-timestamp"/>
|
||||
<ref name="et2-checkbox"/>
|
||||
<ref name="et2-colorpicker"/>
|
||||
<ref name="et2-date"/>
|
||||
<ref name="et2-date-duration"/>
|
||||
@ -82,6 +82,7 @@
|
||||
<ref name="et2-description"/>
|
||||
<ref name="et2-label"/>
|
||||
<ref name="et2-dialog"/>
|
||||
<ref name="et2-merge-dialog"/>
|
||||
<ref name="et2-dropdown-button"/>
|
||||
<ref name="et2-email"/>
|
||||
<ref name="et2-favorites"/>
|
||||
@ -4953,6 +4954,134 @@
|
||||
<attribute name="slot"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-checkbox">
|
||||
<element name="et2-checkbox">
|
||||
<ref name="attlist.et2-checkbox"/>
|
||||
<empty/>
|
||||
</element>
|
||||
</define>
|
||||
<define name="attlist.et2-checkbox" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="selectedValue" a:defaultValue="true"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="unselectedValue" a:defaultValue=""/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="label"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="readonly" a:defaultValue="false">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="required" a:defaultValue="false">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="onchange"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="autofocus">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="autocomplete" a:defaultValue="on"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="ariaLabel"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="ariaDescription"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="helpText"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="class"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="disabled" a:defaultValue="false">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="hidden">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="accesskey"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="parentId"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="statustext"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="onclick"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="noLang">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="align"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="data"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="id"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="width"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="height"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="span"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="slot"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-button">
|
||||
<element name="et2-button">
|
||||
<ref name="attlist.et2-button"/>
|
||||
@ -5080,9 +5209,21 @@
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="hideOnReadonly">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-button-icon">
|
||||
<element name="et2-button-icon">
|
||||
@ -5211,9 +5352,21 @@
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="hideOnReadonly">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-button-scroll">
|
||||
<element name="et2-button-scroll">
|
||||
@ -5249,6 +5402,15 @@
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="hideOnReadonly">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-button-timestamp">
|
||||
<element name="et2-button-timestamp">
|
||||
@ -5387,27 +5549,7 @@
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-checkbox">
|
||||
<element name="et2-checkbox">
|
||||
<ref name="attlist.et2-checkbox"/>
|
||||
<empty/>
|
||||
</element>
|
||||
</define>
|
||||
<define name="attlist.et2-checkbox" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="selectedValue" a:defaultValue="true"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="unselectedValue" a:defaultValue=""/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="label"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="readonly" a:defaultValue="false">
|
||||
<attribute name="hideOnReadonly">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
@ -5415,105 +5557,12 @@
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="required" a:defaultValue="false">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="onchange"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="autofocus">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="autocomplete" a:defaultValue="on"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="ariaLabel"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="ariaDescription"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="helpText"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="class"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="disabled" a:defaultValue="false">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="hidden">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="accesskey"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="parentId"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="statustext"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="onclick"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="noLang">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="align"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="data"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="id"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="width"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="height"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="span"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="slot"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-colorpicker">
|
||||
<element name="et2-colorpicker">
|
||||
@ -5633,6 +5682,9 @@
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-date">
|
||||
<element name="et2-date">
|
||||
@ -5782,6 +5834,9 @@
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-date-duration">
|
||||
<element name="et2-date-duration">
|
||||
@ -5949,6 +6004,9 @@
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-date-range">
|
||||
<element name="et2-date-range">
|
||||
@ -6303,6 +6361,9 @@
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-date-timeonly">
|
||||
<element name="et2-date-timeonly">
|
||||
@ -6446,6 +6507,9 @@
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-date-time-today">
|
||||
<element name="et2-date-time-today">
|
||||
@ -6851,6 +6915,86 @@
|
||||
<attribute name="slot"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-merge-dialog">
|
||||
<element name="et2-merge-dialog">
|
||||
<ref name="attlist.et2-merge-dialog"/>
|
||||
<empty/>
|
||||
</element>
|
||||
</define>
|
||||
<define name="attlist.et2-merge-dialog" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="application"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="path"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="class"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="disabled" a:defaultValue="false">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="hidden">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="accesskey"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="parentId"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="statustext"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="label"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="onclick"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="noLang">
|
||||
<choice>
|
||||
<value>false</value>
|
||||
<value>true</value>
|
||||
<value>1</value>
|
||||
</choice>
|
||||
</attribute>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="align"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="data"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="id"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="width"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="height"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="span"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="slot"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-dropdown-button">
|
||||
<element name="et2-dropdown-button">
|
||||
<ref name="attlist.et2-dropdown-button"/>
|
||||
@ -6981,7 +7125,7 @@
|
||||
</define>
|
||||
<define name="attlist.et2-email" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="value" a:defaultValue="[]"/>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="placeholder" a:defaultValue=""/>
|
||||
@ -8342,7 +8486,7 @@
|
||||
<attribute name="data"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value" a:defaultValue="[]"/>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="title" a:defaultValue="Select"/>
|
||||
@ -9403,6 +9547,9 @@
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-textarea">
|
||||
<element name="et2-textarea">
|
||||
@ -9534,9 +9681,15 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="placeholder"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-number">
|
||||
<element name="et2-number">
|
||||
@ -9680,9 +9833,15 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="type"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-password">
|
||||
<element name="et2-password">
|
||||
@ -9826,9 +9985,15 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="type"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-searchbox">
|
||||
<element name="et2-searchbox">
|
||||
@ -9978,14 +10143,22 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="type"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-textbox">
|
||||
<element name="et2-textbox">
|
||||
<ref name="attlist.et2-textbox"/>
|
||||
<empty/>
|
||||
<optional>
|
||||
<ref name="et2-image"/>
|
||||
</optional>
|
||||
</element>
|
||||
</define>
|
||||
<define name="attlist.et2-textbox" combine="interleave">
|
||||
@ -10112,9 +10285,15 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="type"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-tree">
|
||||
<element name="et2-tree">
|
||||
@ -10741,9 +10920,15 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="type"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-url-email">
|
||||
<element name="et2-url-email">
|
||||
@ -10878,9 +11063,15 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="type"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-url-fax">
|
||||
<element name="et2-url-fax">
|
||||
@ -11012,9 +11203,15 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="type"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-url-phone">
|
||||
<element name="et2-url-phone">
|
||||
@ -11146,9 +11343,15 @@
|
||||
<optional>
|
||||
<attribute name="size"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="type"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-vfs-mime">
|
||||
<element name="et2-vfs-mime">
|
||||
@ -11390,7 +11593,7 @@
|
||||
<attribute name="image"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value" a:defaultValue="[]"/>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="title" a:defaultValue="Select"/>
|
||||
@ -11545,7 +11748,7 @@
|
||||
</define>
|
||||
<define name="attlist.et2-vfs-select-dialog" combine="interleave">
|
||||
<optional>
|
||||
<attribute name="value" a:defaultValue="[]"/>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="title" a:defaultValue="Select"/>
|
||||
@ -16295,6 +16498,9 @@
|
||||
<optional>
|
||||
<attribute name="slot"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="summary"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-split">
|
||||
<element name="et2-split">
|
||||
@ -16979,6 +17185,9 @@
|
||||
<optional>
|
||||
<attribute name="tabindex"/>
|
||||
</optional>
|
||||
<optional>
|
||||
<attribute name="value"/>
|
||||
</optional>
|
||||
</define>
|
||||
<define name="et2-nextmatch-header-entry">
|
||||
<element name="et2-nextmatch-header-entry">
|
||||
@ -17299,4 +17508,4 @@
|
||||
</attribute>
|
||||
</optional>
|
||||
</define>
|
||||
</grammar>
|
||||
</grammar>
|
||||
|
70
doc/lotus-structured-text2csv.php
Executable file
70
doc/lotus-structured-text2csv.php
Executable file
@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Convert Lotus Structured Text to CSV to e.g. use CSV Import
|
||||
*
|
||||
* Writes CSV file to stdout with:
|
||||
* - separator: ,
|
||||
* - enclosure: "
|
||||
* - escape: \
|
||||
* - eol: \n
|
||||
*
|
||||
* Header 1,Header 2,"Header, with comma",Header N
|
||||
* Data1,Data2,Data3,DataN
|
||||
* ...
|
||||
*
|
||||
* Usage: ./lotus-structured-text.php <filename>
|
||||
* ./lotus-structured-text.php < <filename>
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli')
|
||||
{
|
||||
die('This script can only be run from the command line.');
|
||||
}
|
||||
|
||||
if (!isset($argv[1]))
|
||||
{
|
||||
$fp = fopen('php://stdin', 'r');
|
||||
}
|
||||
elseif (!($fp = fopen($argv[1], 'r')))
|
||||
{
|
||||
die("Unable to open '$argv[1]'!");
|
||||
}
|
||||
|
||||
$ctrl_l = chr(ord("L")-ord("@"));
|
||||
$records = $record = $keys = [];
|
||||
while (($line = fgets($fp)) !== false)
|
||||
{
|
||||
$line = trim($line);
|
||||
if (($line === '' || $line === $ctrl_l))
|
||||
{
|
||||
if ($record)
|
||||
{
|
||||
if (!$keys || array_diff(array_keys($record), $keys))
|
||||
{
|
||||
$keys = array_unique(array_merge($keys, array_keys($record)));
|
||||
}
|
||||
$records[] = $record;
|
||||
}
|
||||
$record = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
list($key, $value) = preg_split('/: */', $line, 2)+['', ''];
|
||||
$record[$key] = $value;
|
||||
}
|
||||
}
|
||||
if ($record)
|
||||
{
|
||||
if (!$keys || array_diff(array_keys($record), $keys))
|
||||
{
|
||||
$keys = array_unique(array_merge($keys, array_keys($record)));
|
||||
}
|
||||
$records[] = $record;
|
||||
}
|
||||
fputcsv(STDOUT, $keys);
|
||||
foreach($records as $record)
|
||||
{
|
||||
fputcsv(STDOUT, array_map(static function($key) use ($record) {
|
||||
return $record[$key] ?? '';
|
||||
}, $keys));
|
||||
}
|
35
doc/merge-cal-events-by-uid.sh
Executable file
35
doc/merge-cal-events-by-uid.sh
Executable file
@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# This script merges multiple events with same uid existing due to importing each users calendar without participant or chair information.
|
||||
#
|
||||
# The event-owner is kinda random, as events are merged in the one with the smallest cal_id / earliest imported!
|
||||
#
|
||||
|
||||
DB=${1:-egroupware}
|
||||
|
||||
# merge multiple single events or series masters with same uid
|
||||
mysql $DB --execute "SELECT min(cal_id),cal_uid FROM egw_cal where cal_reference=0 group by cal_uid having count(*) > 1" --skip-column-names |
|
||||
while read ID UUID; do
|
||||
#echo "#$ID: $UUID"
|
||||
cat << EOF | mysql $DB
|
||||
update ignore egw_cal_user set cal_id=$ID where cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference=0);
|
||||
delete from egw_cal_user where cal_id<>$ID AND cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference=0);
|
||||
update ignore egw_cal_dates set cal_id=$ID where cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference=0);
|
||||
delete from egw_cal_dates where cal_id<>$ID AND cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference=0);
|
||||
delete from egw_cal_repeats where cal_id<>$ID AND cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference=0);
|
||||
update egw_cal set cal_reference=$ID where cal_reference<>0 and cal_uid='$UUID';
|
||||
delete from egw_cal where cal_id<>$ID AND cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference=0);
|
||||
EOF
|
||||
done
|
||||
|
||||
# merge multiple recurrences/exceptions at same time
|
||||
mysql $DB --execute "SELECT min(cal_id),cal_uid,cal_recurrence FROM egw_cal where cal_reference<>0 group by cal_uid,cal_recurrence having count(*) > 1" --skip-column-names |
|
||||
while read ID UUID RECURRENCE; do
|
||||
#echo "#$ID: $UUID: RECURRENCE"
|
||||
cat << EOF | mysql $DB
|
||||
update ignore egw_cal_user set cal_id=$ID where cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference<>0 and cal_recurrence=$RECURRENCE);
|
||||
delete from egw_cal_user where cal_id<>$ID AND cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference<>0 and cal_recurrence=$RECURRENCE);
|
||||
delete from egw_cal_dates where cal_id<>$ID AND cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference<>0 and cal_recurrence=$RECURRENCE);
|
||||
delete from egw_cal where cal_id<>$ID AND cal_id in (select cal_id from egw_cal where cal_uid='$UUID' and cal_reference<>0 and cal_recurrence=$RECURRENCE);
|
||||
EOF
|
||||
done
|
@ -13,7 +13,8 @@ egw-framework#egw_fw_basecontainer {
|
||||
/* Internals */
|
||||
|
||||
&::part(status-split) {
|
||||
--max: 150px;
|
||||
/* This limits the max size of the status panel */
|
||||
--max: 32px;
|
||||
}
|
||||
|
||||
&::part(header) {
|
||||
@ -60,6 +61,10 @@ egw-framework#egw_fw_basecontainer {
|
||||
div#egw_fw_sidebar_r {
|
||||
position: initial;
|
||||
top: initial;
|
||||
|
||||
et2-avatar, et2-lavatar {
|
||||
--size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.egw_fw_ui_sidemenu_entry_header {
|
||||
@ -69,14 +74,6 @@ egw-framework#egw_fw_basecontainer {
|
||||
}
|
||||
}
|
||||
|
||||
egw-app {
|
||||
&::part(name) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/*** HEADER ***/
|
||||
#egw_fw_topmenu_info_items {
|
||||
display: flex;
|
||||
@ -164,6 +161,61 @@ egw-app {
|
||||
|
||||
/*** END HEADER ***/
|
||||
|
||||
div#egw_fw_basecontainer {
|
||||
display: none;
|
||||
/*** APPLICATION ***/
|
||||
egw-app {
|
||||
&::part(name) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&::part(content) {
|
||||
flex-direction: column;
|
||||
overflow-y:hidden;
|
||||
}
|
||||
|
||||
& > div[id] > div {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*** END APPLICATION ***/
|
||||
|
||||
/*** WIDGETS ***/
|
||||
/* This should mostly go away with webcomponents */
|
||||
|
||||
/* Get nextmatch sizing more nicely without messing with dynheight */
|
||||
div.et2_nextmatch {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
& > div:not(:first-child) {
|
||||
flex: 1 1 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.egwGridView_outer {
|
||||
height: 100%;
|
||||
|
||||
thead tr th.optcol span.selectcols {
|
||||
height: 9px;
|
||||
padding: 4px 14px 0 2px;
|
||||
margin-top: 4px;
|
||||
background-image: url(../../../api/templates/default/images/selectcols.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 10px 10px;
|
||||
display: inline-block;
|
||||
background-position: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.egwGridView_scrollarea {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
@ -41,15 +41,12 @@
|
||||
<div slot="header-right" id="egw_fw_topmenu_info_items">
|
||||
{topmenu_info_items}
|
||||
</div>
|
||||
<div slot="header">
|
||||
<sl-switch slot="header" id="placeholders" checked>Placeholders</sl-switch>
|
||||
</div>
|
||||
|
||||
<!-- status app is looking for this -->
|
||||
<div slot="aside" id="egw_fw_sidebar_r"></div>
|
||||
<!-- status app is looking for this, but it could slot itself -->
|
||||
<div slot="status" id="egw_fw_sidebar_r"></div>
|
||||
|
||||
<!-- Currently open app -->
|
||||
<egw-app name="{open_app_name}" url="{open_app_url}" active></egw-app>
|
||||
<egw-app id="{open_app_name}" name="{open_app_name}" url="{open_app_url}" active></egw-app>
|
||||
</egw-framework>
|
||||
|
||||
|
||||
|
@ -67,7 +67,7 @@ class kdots_framework extends Api\Framework\Ajax
|
||||
|
||||
parent::topmenu($vars, $apps);
|
||||
|
||||
$vars['topmenu_items'] = "<sl-menu>" . implode("\n", $this->topmenu_items) . "</sl-menu>";
|
||||
$vars['topmenu_items'] = "<sl-menu id='egw_fw_topmenu_items'>" . implode("\n", $this->topmenu_items) . "</sl-menu>";
|
||||
$vars['topmenu_info_items'] = '';
|
||||
foreach($this->topmenu_info_items as $id => $item)
|
||||
{
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {css, html, LitElement, nothing} from "lit";
|
||||
import {css, html, LitElement, nothing, PropertyValues} from "lit";
|
||||
import {customElement} from "lit/decorators/custom-element.js";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {classMap} from "lit/directives/class-map.js";
|
||||
@ -8,6 +8,7 @@ import styles from "./EgwFramework.styles";
|
||||
import {egw} from "../../api/js/jsapi/egw_global";
|
||||
import {SlDropdown, SlTab, SlTabGroup} from "@shoelace-style/shoelace";
|
||||
import {EgwFrameworkApp} from "./EgwFrameworkApp";
|
||||
import {until} from "lit/directives/until.js";
|
||||
|
||||
/**
|
||||
* @summary Accessable, webComponent-based EGroupware framework
|
||||
@ -95,11 +96,9 @@ export class EgwFramework extends LitElement
|
||||
|
||||
/**
|
||||
* This is the list of all applications we know about
|
||||
*
|
||||
* @type {any[]}
|
||||
*/
|
||||
@property({type: Array, attribute: "application-list"})
|
||||
applicationList = [];
|
||||
applicationList : ApplicationInfo[] = [];
|
||||
|
||||
private get tabs() : SlTabGroup { return this.shadowRoot.querySelector("sl-tab-group");}
|
||||
|
||||
@ -109,7 +108,7 @@ export class EgwFramework extends LitElement
|
||||
if(this.egw.window && this.egw.window.opener == null && !this.egw.window.framework)
|
||||
{
|
||||
// This works, but stops a lot else from working
|
||||
//this.egw.window.framework = this;
|
||||
this.egw.window.framework = this;
|
||||
}
|
||||
if(this.egw.window?.framework && this.egw.window?.framework !== this)
|
||||
{
|
||||
@ -118,6 +117,40 @@ export class EgwFramework extends LitElement
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties : PropertyValues)
|
||||
{
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
// Load hidden apps like status, as long as they can be loaded
|
||||
this.applicationList.forEach((app) =>
|
||||
{
|
||||
if(app.status == "5" && app.url && !app.url.match(/menuaction\=none/))
|
||||
{
|
||||
this.loadApp(app.name);
|
||||
}
|
||||
});
|
||||
// Load additional tabs
|
||||
Object.values(this.tabApps).forEach(app => this.loadApp(app.name));
|
||||
|
||||
// Init timer
|
||||
this.egw.add_timer('topmenu_info_timer');
|
||||
}
|
||||
|
||||
/**
|
||||
* Special tabs that are not directly associated with an application (CRM)
|
||||
* @type {[]}
|
||||
* @private
|
||||
*/
|
||||
protected get tabApps() : { [id : string] : ApplicationInfo }
|
||||
{
|
||||
return JSON.parse(egw.getSessionItem('api', 'fw_tab_apps') || null) || {};
|
||||
}
|
||||
|
||||
protected set tabApps(apps : { [id : string] : ApplicationInfo })
|
||||
{
|
||||
egw.setSessionItem('api', 'fw_tab_apps', JSON.stringify(apps));
|
||||
}
|
||||
|
||||
get egw() : typeof egw
|
||||
{
|
||||
return window.egw ?? <typeof egw>{
|
||||
@ -128,6 +161,21 @@ export class EgwFramework extends LitElement
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise for if egw is loaded
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
getEgwComplete()
|
||||
{
|
||||
let egwLoading = Promise.resolve();
|
||||
if(typeof this.egw.window['egw_ready'] !== "undefined")
|
||||
{
|
||||
egwLoading = this.egw.window['egw_ready'];
|
||||
}
|
||||
return egwLoading;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param _function Framework function to be called on the server.
|
||||
@ -153,10 +201,16 @@ export class EgwFramework extends LitElement
|
||||
(menuaction ? '.' + menuaction[1] : '');
|
||||
};
|
||||
|
||||
public getApplicationByName(appName)
|
||||
{
|
||||
return this.querySelector(`egw-app[name="${appName}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an application into the framework
|
||||
*
|
||||
* Loading is done by name, and we look up everything we need in the applicationList
|
||||
* Loading is done by name, and we look up everything we need in the applicationList.
|
||||
* If already loaded, this just returns the existing EgwFrameworkApp, optionally activated & with new URL loaded.
|
||||
*
|
||||
* @param {string} appname
|
||||
* @param {boolean} active
|
||||
@ -165,7 +219,7 @@ export class EgwFramework extends LitElement
|
||||
*/
|
||||
public loadApp(appname : string, active = false, url = null) : EgwFrameworkApp
|
||||
{
|
||||
const existing : EgwFrameworkApp = this.querySelector(`egw-app[name="${appname}"]`);
|
||||
const existing : EgwFrameworkApp = this.querySelector(`egw-app[id="${appname}"]`);
|
||||
if(existing)
|
||||
{
|
||||
if(active)
|
||||
@ -174,37 +228,69 @@ export class EgwFramework extends LitElement
|
||||
}
|
||||
if(url)
|
||||
{
|
||||
existing.url = url;
|
||||
existing.load(url);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
const app = this.applicationList.find(a => a.name == appname);
|
||||
const app = this.applicationList.find(a => a.name == appname) ??
|
||||
this.tabApps[appname];
|
||||
|
||||
if(!app)
|
||||
{
|
||||
console.log("Cannot load unknown app '" + appname + "'");
|
||||
return null;
|
||||
}
|
||||
let appComponent = <EgwFrameworkApp>document.createElement("egw-app");
|
||||
appComponent.id = appname;
|
||||
appComponent.name = appname;
|
||||
appComponent.setAttribute("id", appname);
|
||||
appComponent.setAttribute("name", app.internalName || appname);
|
||||
appComponent.url = url ?? app?.url;
|
||||
if(app.title)
|
||||
{
|
||||
appComponent.title = app.title;
|
||||
}
|
||||
|
||||
this.append(appComponent);
|
||||
// App was not in the tab list
|
||||
if(typeof app.opened == "undefined")
|
||||
{
|
||||
app.opened = this.shadowRoot.querySelectorAll("sl-tab").length;
|
||||
// Need to update tabApps directly, reference doesn't work
|
||||
if(typeof this.tabApps[appname] == "object")
|
||||
{
|
||||
let tabApps = {...this.tabApps};
|
||||
tabApps[appname] = app;
|
||||
this.tabApps = tabApps;
|
||||
}
|
||||
this.requestUpdate("applicationList");
|
||||
}
|
||||
|
||||
// Wait until new tab is there to activate it
|
||||
if(active)
|
||||
if(active || app.active)
|
||||
{
|
||||
this.updateComplete.then(() =>
|
||||
// Wait for egw
|
||||
this.getEgwComplete().then(() =>
|
||||
{
|
||||
this.tabs.show(appname);
|
||||
})
|
||||
// Wait for redraw after getEgwComplete promise
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
// Tabs present
|
||||
this.updateComplete.then(() =>
|
||||
{
|
||||
this.showTab(appname);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return appComponent;
|
||||
}
|
||||
|
||||
public get activeApp() : EgwFrameworkApp
|
||||
{
|
||||
return this.querySelector("egw-app[active]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a link into the framework
|
||||
*
|
||||
@ -214,7 +300,7 @@ export class EgwFramework extends LitElement
|
||||
*/
|
||||
public linkHandler(_link : string, _app : string)
|
||||
{
|
||||
//Determine the app string from the application parameter
|
||||
// Determine the app string from the application parameter
|
||||
let app = null;
|
||||
if(_app && typeof _app == 'string')
|
||||
{
|
||||
@ -235,13 +321,13 @@ export class EgwFramework extends LitElement
|
||||
// add target flag
|
||||
_link += '&target=_tab';
|
||||
const appname = app.appName + ":" + btoa(_link);
|
||||
this.applicationList[appname] = {...app};
|
||||
this.applicationList[appname]['name'] = appname;
|
||||
this.applicationList[appname]['indexUrl'] = _link;
|
||||
this.applicationList[appname]['tab'] = null;
|
||||
this.applicationList[appname]['browser'] = null;
|
||||
this.applicationList[appname]['title'] = 'view';
|
||||
app = this.applicationList[appname];
|
||||
this.applicationList.push({
|
||||
...app,
|
||||
name: appname,
|
||||
url: _link,
|
||||
title: 'view'
|
||||
});
|
||||
app = this.applicationList[this.applicationList.length - 1];
|
||||
}
|
||||
this.loadApp(app.name, true, _link);
|
||||
}
|
||||
@ -261,6 +347,111 @@ export class EgwFramework extends LitElement
|
||||
}
|
||||
}
|
||||
|
||||
public tabLinkHandler(_link : string, _extra = {
|
||||
id: ""
|
||||
})
|
||||
{
|
||||
const app = this.parseAppFromUrl(_link);
|
||||
if(app)
|
||||
{
|
||||
const appname = app.name + "-" + btoa(_extra.id ? _extra.id : _link).replace(/=/g, 'i');
|
||||
if(this.getApplicationByName(appname))
|
||||
{
|
||||
this.loadApp(appname, true, _link);
|
||||
return appname;
|
||||
}
|
||||
|
||||
// add target flag
|
||||
_link += '&fw_target=' + appname;
|
||||
// create an actual clone of existing app object
|
||||
let clone = {
|
||||
...app,
|
||||
..._extra,
|
||||
//isFrameworkTab: true, ??
|
||||
name: appname,
|
||||
internalName: app.name,
|
||||
url: _link,
|
||||
// Need to override to open, base app might already be opened
|
||||
opened: undefined
|
||||
};
|
||||
// Store only in session
|
||||
let tabApps = {...this.tabApps};
|
||||
tabApps[appname] = clone;
|
||||
this.tabApps = tabApps;
|
||||
|
||||
/* ??
|
||||
this.applications[appname]['sidemenuEntry'] = this.sidemenuUi.addEntry(
|
||||
this.applications[appname].displayName, this.applications[appname].icon,
|
||||
function()
|
||||
{
|
||||
self.applicationTabNavigate(self.applications[appname], _link, false, -1, null);
|
||||
}, this.applications[appname], appname);
|
||||
|
||||
*/
|
||||
this.loadApp(appname, true);
|
||||
|
||||
return appname;
|
||||
}
|
||||
else
|
||||
{
|
||||
egw_alertHandler("No appropriate target application has been found.",
|
||||
"Target link: " + _link);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a (centered) popup window with given size and url
|
||||
*
|
||||
* @param {string} _url
|
||||
* @param {number} _width
|
||||
* @param {number} _height
|
||||
* @param {string} _windowName or "_blank"
|
||||
* @param {string|boolean} _app app-name for framework to set correct opener or false for current app
|
||||
* @param {boolean} _returnID true: return window, false: return undefined
|
||||
* @param {type} _status "yes" or "no" to display status bar of popup
|
||||
* @param {DOMWindow} _parentWnd parent window
|
||||
* @returns {DOMWindow|undefined}
|
||||
*/
|
||||
public openPopup(_url, _width, _height, _windowName, _app, _returnID, _status, _parentWnd)
|
||||
{
|
||||
//Determine the window the popup should be opened in - normally this is the iframe of the currently active application
|
||||
let parentWindow = _parentWnd || window;
|
||||
let navigate = false;
|
||||
let appEntry = null;
|
||||
if(typeof _app != 'undefined' && _app !== false)
|
||||
{
|
||||
appEntry = this.getApplicationByName(_app);
|
||||
if(appEntry && appEntry.browser == null)
|
||||
{
|
||||
navigate = true;
|
||||
this.applicationTabNavigate(appEntry, appEntry.indexUrl);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
appEntry = this.activeApp;
|
||||
}
|
||||
|
||||
if(appEntry != null && appEntry.useIframe && (_app || !egw(parentWindow).is_popup()))
|
||||
{
|
||||
parentWindow = appEntry.iframe.contentWindow;
|
||||
}
|
||||
|
||||
const windowID = egw(parentWindow).openPopup(_url, _width, _height, _windowName, _app, true, _status, true);
|
||||
|
||||
windowID.framework = this;
|
||||
|
||||
if(navigate)
|
||||
{
|
||||
window.setTimeout("framework.applicationTabNavigate(framework.activeApp, framework.activeApp.indexUrl);", 500);
|
||||
}
|
||||
|
||||
if(_returnID !== false)
|
||||
{
|
||||
return windowID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to obtain the application from a menuaction
|
||||
* @param {string} _url
|
||||
@ -287,7 +478,7 @@ export class EgwFramework extends LitElement
|
||||
*/
|
||||
public async print()
|
||||
{
|
||||
const appElement : EgwFrameworkApp = this.querySelector("egw-app[active]");
|
||||
const appElement : EgwFrameworkApp = this.activeApp;
|
||||
try
|
||||
{
|
||||
if(appElement)
|
||||
@ -318,11 +509,15 @@ export class EgwFramework extends LitElement
|
||||
* @protected
|
||||
*/
|
||||
protected handleApplicationTabShow(event)
|
||||
{
|
||||
// Create & show app
|
||||
this.showTab(event.target.activeTab.panel);
|
||||
}
|
||||
|
||||
public showTab(appname)
|
||||
{
|
||||
this.querySelectorAll("egw-app").forEach(app => app.removeAttribute("active"));
|
||||
|
||||
// Create & show app
|
||||
const appname = event.target.activeTab.panel;
|
||||
let appComponent = this.querySelector(`egw-app#${appname}`);
|
||||
if(!appComponent)
|
||||
{
|
||||
@ -331,7 +526,11 @@ export class EgwFramework extends LitElement
|
||||
appComponent.setAttribute("active", "");
|
||||
|
||||
// Update the list on the server
|
||||
this.updateTabs(event.target.activeTab);
|
||||
const tabGroup : SlTabGroup = this.shadowRoot.querySelector("sl-tab-group.egw_fw__open_applications");
|
||||
tabGroup.updateComplete.then(() =>
|
||||
{
|
||||
this.updateTabs(appComponent);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -351,22 +550,73 @@ export class EgwFramework extends LitElement
|
||||
else
|
||||
{
|
||||
// Show will update, but closing in the background we call directly
|
||||
this.updateTabs(tabGroup.querySelector("sl-tab[active]"));
|
||||
tabGroup.updateComplete.then(() =>
|
||||
{
|
||||
this.updateTabs(tabGroup.querySelector("sl-tab[active]"));
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the tab + panel
|
||||
tab.remove();
|
||||
panel.remove();
|
||||
if(panel)
|
||||
{
|
||||
panel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store last status of tabs
|
||||
* tab status being used in order to open all previous opened
|
||||
* tabs and to activate the last active tab
|
||||
*/
|
||||
private updateTabs(activeTab)
|
||||
{
|
||||
let appList = [];
|
||||
//Send the current tab list to the server
|
||||
let data = this.assembleTabList(activeTab);
|
||||
|
||||
// Update session tabs
|
||||
let tabs = {};
|
||||
Object.keys(this.tabApps).forEach((t) =>
|
||||
{
|
||||
if(data.some(d => d.appName == t))
|
||||
{
|
||||
tabs[t] = this.tabApps[t];
|
||||
tabs[t].active = t == activeTab.id;
|
||||
}
|
||||
});
|
||||
this.tabApps = tabs;
|
||||
|
||||
|
||||
//Serialize the tab list and check whether it really has changed since the last
|
||||
//submit
|
||||
var serialized = egw.jsonEncode(data);
|
||||
if(serialized != this.serializedTabState)
|
||||
{
|
||||
this.serializedTabState = serialized;
|
||||
if(this.tabApps)
|
||||
{
|
||||
this._setTabAppsSession(this.tabApps);
|
||||
}
|
||||
egw.jsonq('EGroupware\\Api\\Framework\\Ajax::ajax_tab_changed_state', [data]);
|
||||
}
|
||||
}
|
||||
|
||||
private assembleTabList(activeTab)
|
||||
{
|
||||
let appList = []
|
||||
Array.from(this.shadowRoot.querySelectorAll("sl-tab-group.egw_fw__open_applications sl-tab")).forEach((tab : SlTab) =>
|
||||
{
|
||||
appList.push({appName: tab.panel, active: activeTab.panel == tab.panel})
|
||||
appList.push({appName: tab.panel, active: activeTab.id == tab.panel})
|
||||
});
|
||||
this.egw.jsonq('EGroupware\\Api\\Framework\\Ajax::ajax_tab_changed_state', [appList]);
|
||||
return appList;
|
||||
}
|
||||
|
||||
private _setTabAppsSession(_tabApps)
|
||||
{
|
||||
if(_tabApps)
|
||||
{
|
||||
egw.setSessionItem('api', 'fw_tab_apps', JSON.stringify(_tabApps));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -416,7 +666,7 @@ export class EgwFramework extends LitElement
|
||||
}
|
||||
classes[`egw_fw__layout-${this.layout}`] = true;
|
||||
|
||||
return html`
|
||||
return html`${until(this.getEgwComplete().then(() => html`
|
||||
<div class=${classMap(classes)} part="base">
|
||||
<div class="egw_fw__banner" part="banner" role="banner">
|
||||
<slot name="banner"><span class="placeholder">Banner</span></slot>
|
||||
@ -435,8 +685,8 @@ export class EgwFramework extends LitElement
|
||||
@sl-tab-show=${this.handleApplicationTabShow}
|
||||
@sl-close=${this.handleApplicationTabClose}
|
||||
>
|
||||
${repeat(this.applicationList
|
||||
.filter(app => typeof app.opened !== "undefined")
|
||||
${repeat([...this.applicationList, ...Object.values(this.tabApps)]
|
||||
.filter(app => typeof app.opened !== "undefined" && app.status !== "5")
|
||||
.sort((a, b) => a.opened - b.opened), (app) => this._applicationTabTemplate(app))}
|
||||
</sl-tab-group>
|
||||
<slot name="header"><span class="placeholder">header</span></slot>
|
||||
@ -460,6 +710,28 @@ export class EgwFramework extends LitElement
|
||||
<slot name="footer"><span class="placeholder">footer</span></slot>
|
||||
</footer>
|
||||
</div>
|
||||
`;
|
||||
`), html`<span>Waiting for egw...</span>
|
||||
<slot></slot>`)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information we keep and use about each app on the client side
|
||||
* This might not be limited to actual EGw apps,
|
||||
*/
|
||||
export interface ApplicationInfo
|
||||
{
|
||||
/* Internal name, used for reference & indexing. Might not be an egw app, might have extra bits */
|
||||
name : string,
|
||||
/* Must be an egw app, used for the EgwFrameworkApp, preferences, etc. */
|
||||
internalName? : string,
|
||||
icon : string
|
||||
title : string,
|
||||
url : string,
|
||||
/* What type of application (1: normal, 5: loaded but no tab) */
|
||||
status : string,// = "1",
|
||||
/* Is the app open, and at what place in the tab list */
|
||||
opened? : number,
|
||||
/* Is the app currently active */
|
||||
active? : boolean// = false
|
||||
}
|
@ -137,20 +137,26 @@ export default css`
|
||||
@media (min-width: 600px) {
|
||||
.egw_fw_app__main {
|
||||
grid-template-columns: [start left] min-content [ main] 1fr [right] min-content [end];
|
||||
grid-template-rows: [start sub-header] fit-content(2em) [main] auto [footer] fit-content(2em) [end];
|
||||
grid-template-rows: [start sub-header] fit-content(2em) [main] auto [footer] fit-content(4em) [end];
|
||||
}
|
||||
|
||||
.egw_fw_app__aside {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.egw_fw_app__aside_content {
|
||||
.egw_fw_app__aside_content, .egw_fw_app__main_content {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.egw_fw_app__main_content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
::slotted(*) {
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
::slotted(iframe) {
|
||||
@ -207,5 +213,12 @@ export default css`
|
||||
}
|
||||
}
|
||||
|
||||
/* End layout */
|
||||
|
||||
/* Styling */
|
||||
|
||||
.egw_fw_app__header sl-icon[name="three-dots-vertical"] {
|
||||
padding: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
`
|
@ -11,6 +11,8 @@ import {HasSlotController} from "../../api/js/etemplate/Et2Widget/slot";
|
||||
import type {EgwFramework} from "./EgwFramework";
|
||||
import {etemplate2} from "../../api/js/etemplate/etemplate2";
|
||||
import {et2_IPrint} from "../../api/js/etemplate/et2_core_interfaces";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {until} from "lit/directives/until.js";
|
||||
|
||||
/**
|
||||
* @summary Application component inside EgwFramework
|
||||
@ -89,9 +91,12 @@ export class EgwFrameworkApp extends LitElement
|
||||
];
|
||||
}
|
||||
|
||||
@property()
|
||||
@property({reflect: true})
|
||||
name = "Application name";
|
||||
|
||||
@property()
|
||||
title : string = "";
|
||||
|
||||
@property()
|
||||
url = "";
|
||||
|
||||
@ -133,6 +138,7 @@ export class EgwFrameworkApp extends LitElement
|
||||
|
||||
/** The application's content must be in an iframe instead of handled normally */
|
||||
protected useIframe = false;
|
||||
protected _sideboxData : any;
|
||||
|
||||
connectedCallback()
|
||||
{
|
||||
@ -145,11 +151,14 @@ export class EgwFrameworkApp extends LitElement
|
||||
{
|
||||
this.rightPanelInfo.preferenceWidth = typeof width !== "undefined" ? parseInt(width) : this.rightPanelInfo.defaultWidth;
|
||||
});
|
||||
|
||||
this.addEventListener("load", this.handleEtemplateLoad);
|
||||
}
|
||||
|
||||
disconnectedCallback()
|
||||
{
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("load", this.handleEtemplateLoad);
|
||||
}
|
||||
|
||||
firstUpdated()
|
||||
@ -165,7 +174,7 @@ export class EgwFrameworkApp extends LitElement
|
||||
return result
|
||||
}
|
||||
|
||||
protected load(url)
|
||||
public load(url)
|
||||
{
|
||||
if(!url)
|
||||
{
|
||||
@ -175,6 +184,7 @@ export class EgwFrameworkApp extends LitElement
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.url = url;
|
||||
let targetUrl = "";
|
||||
this.useIframe = true;
|
||||
let matches = url.match(/\/index.php\?menuaction=([A-Za-z0-9_\.]*.*&ajax=true.*)$/);
|
||||
@ -199,22 +209,26 @@ export class EgwFrameworkApp extends LitElement
|
||||
return this.loadingPromise = this.egw.request(
|
||||
this.framework.getMenuaction('ajax_exec', targetUrl, this.name),
|
||||
[targetUrl]
|
||||
).then((data : string[]) =>
|
||||
).then((data : string | string[] | { DOMNodeID? : string } | { DOMNodeID? : string }[]) =>
|
||||
{
|
||||
if(!data)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Load request returns HTML. Shove it in.
|
||||
if(typeof data == "string" || typeof data == "object" && typeof data[0] == "string")
|
||||
{
|
||||
render(html`${unsafeHTML(data.join(""))}`, this);
|
||||
render(html`${unsafeHTML((<string[]>data).join(""))}`, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We got some data, use it
|
||||
if(data.DOMNodeID)
|
||||
{
|
||||
this.id = data.DOMNodeID;
|
||||
}
|
||||
const items = (Array.isArray(data) ? data : [data])
|
||||
.filter(data => (typeof data.DOMNodeID == "string" && document.querySelector("[id='" + data.DOMNodeID + "']") == null));
|
||||
|
||||
render(html`${repeat(items, i => i.DOMNodeID, (item) => html`
|
||||
<div id="${item.DOMNodeID}"></div>`)}`, this);
|
||||
}
|
||||
this.addEventListener("load", this.handleEtemplateLoad, {once: true});
|
||||
|
||||
// Might have just slotted aside content, hasSlotController will requestUpdate()
|
||||
// but we need to do it anyway for translation
|
||||
@ -226,13 +240,12 @@ export class EgwFrameworkApp extends LitElement
|
||||
this.loadingPromise = new Promise((resolve, reject) =>
|
||||
{
|
||||
const timeout = setTimeout(() => reject(this.name + " load failed"), 5000);
|
||||
this.addEventListener("load", () =>
|
||||
render(this._iframeTemplate(), this);
|
||||
this.querySelector("iframe").addEventListener("load", () =>
|
||||
{
|
||||
clearTimeout(timeout);
|
||||
resolve()
|
||||
}, {once: true});
|
||||
|
||||
render(this._iframeTemplate(), this);
|
||||
});
|
||||
// Might have just changed useIFrame, need to update to show that
|
||||
this.requestUpdate();
|
||||
@ -240,9 +253,15 @@ export class EgwFrameworkApp extends LitElement
|
||||
}
|
||||
}
|
||||
|
||||
public getMenuaction(_fun, _ajax_exec_url, appName = "")
|
||||
{
|
||||
return this.framework.getMenuaction(_fun, _ajax_exec_url, appName || this.name);
|
||||
}
|
||||
|
||||
public setSidebox(sideboxData, hash?)
|
||||
{
|
||||
|
||||
this._sideboxData = sideboxData;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public showLeft()
|
||||
@ -300,9 +319,9 @@ export class EgwFrameworkApp extends LitElement
|
||||
this.egw.loading_prompt(this.name, true, this.egw.lang('please wait...'), this, egwIsMobile() ? 'horizental' : 'spinner');
|
||||
|
||||
// Give framework a chance to deal, then reset the etemplates
|
||||
appWindow.setTimeout(function()
|
||||
appWindow.setTimeout(() =>
|
||||
{
|
||||
for(var i = 0; i < et2_list.length; i++)
|
||||
for(let i = 0; i < et2_list.length; i++)
|
||||
{
|
||||
et2_list[i].widgetContainer.iterateOver(function(_widget)
|
||||
{
|
||||
@ -362,7 +381,7 @@ export class EgwFrameworkApp extends LitElement
|
||||
|
||||
get egw()
|
||||
{
|
||||
return window.egw ?? (<EgwFramework>this.parentElement).egw ?? null;
|
||||
return window.egw(this.name) ?? (<EgwFramework>this.parentElement).egw ?? null;
|
||||
}
|
||||
|
||||
get framework() : EgwFramework
|
||||
@ -370,6 +389,11 @@ export class EgwFrameworkApp extends LitElement
|
||||
return this.closest("egw-framework");
|
||||
}
|
||||
|
||||
get appName() : string
|
||||
{
|
||||
return this.name;
|
||||
}
|
||||
|
||||
private hasSideContent(side : "left" | "right")
|
||||
{
|
||||
return this.hasSlotController.test(`${side}-header`) ||
|
||||
@ -382,8 +406,8 @@ export class EgwFrameworkApp extends LitElement
|
||||
*/
|
||||
protected handleEtemplateLoad(event)
|
||||
{
|
||||
const etemplate = etemplate2.getById(this.id);
|
||||
if(!etemplate)
|
||||
const etemplate = etemplate2.getById(event.target.id);
|
||||
if(!etemplate || !event.composedPath().includes(this))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -418,16 +442,25 @@ export class EgwFrameworkApp extends LitElement
|
||||
this[`${event.target.panelInfo.side}Collapsed`] = newPosition == event.target.panelInfo.hiddenWidth;
|
||||
|
||||
let preferenceName = event.target.panelInfo.preference;
|
||||
if(newPosition != event.target.panelInfo.preferenceWidth)
|
||||
if(newPosition != event.target.panelInfo.preferenceWidth && !isNaN(newPosition))
|
||||
{
|
||||
event.target.panelInfo.preferenceWidth = newPosition;
|
||||
if(this.resizeTimeout)
|
||||
{
|
||||
window.clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
window.setTimeout(() =>
|
||||
this.resizeTimeout = window.setTimeout(() =>
|
||||
{
|
||||
this.egw.set_preference(this.name, preferenceName, newPosition);
|
||||
|
||||
// Tell etemplates to resize
|
||||
this.querySelectorAll("[id]").forEach(e =>
|
||||
{
|
||||
if(etemplate2.getById(e.id))
|
||||
{
|
||||
etemplate2.getById(e.id).resize(new Event("resize"));
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@ -497,36 +530,109 @@ export class EgwFrameworkApp extends LitElement
|
||||
protected _rightHeaderTemplate()
|
||||
{
|
||||
return html`
|
||||
<sl-button-group>
|
||||
<et2-button-icon nosubmit name="arrow-clockwise"
|
||||
label=${this.egw.lang("Reload %1", this.egw.lang(this.name))}
|
||||
statustext=${this.egw.lang("Reload %1", this.egw.lang(this.name))}
|
||||
@click=${(e) =>
|
||||
{
|
||||
this.egw.refresh("", this.name);
|
||||
/* Could also be this.load(false); this.load(this.url) */
|
||||
}}
|
||||
></et2-button-icon>
|
||||
<et2-button-icon nosubmit name="printer"
|
||||
label=${this.egw.lang("Print")}
|
||||
statustext=${this.egw.lang("Print")}
|
||||
@click=${(e) => this.framework.print()}
|
||||
></et2-button-icon>
|
||||
${this.egw.user('apps')['waffles'] !== "undefined" ? html`
|
||||
<et2-button-icon nosubmit name="gear-wide"
|
||||
label=${this.egw.lang("Site configuration for %1", this.egw.lang(this.name))}
|
||||
statustext=${this.egw.lang("App configuration")}
|
||||
@click=${(e) =>
|
||||
{
|
||||
// @ts-ignore
|
||||
egw_link_handler(`/egroupware/index.php?menuaction=admin.admin_ui.index&load=admin.uiconfig.index&appname=${this.name}&ajax=true`, 'admin');
|
||||
}}
|
||||
></et2-button-icon>` : nothing
|
||||
}
|
||||
</sl-button-group>
|
||||
<sl-tooltip content=${this.egw.lang("Application menu")}>
|
||||
<sl-dropdown>
|
||||
<sl-icon slot="trigger" name="three-dots-vertical"></sl-icon>
|
||||
<sl-menu>
|
||||
<sl-menu-item
|
||||
@click=${(e) =>
|
||||
{
|
||||
this.egw.refresh("", this.name);
|
||||
/* Could also be this.load(false); this.load(this.url) */
|
||||
}}
|
||||
>
|
||||
<sl-icon slot="prefix" name="arrow-clockwise"></sl-icon>
|
||||
${this.egw.lang("Reload %1", this.egw.lang(this.name))}
|
||||
</sl-menu-item>
|
||||
<sl-menu-item
|
||||
@click=${(e) => this.framework.print()}
|
||||
>
|
||||
<sl-icon slot="prefix" name="printer"></sl-icon>
|
||||
${this.egw.lang("Print")}
|
||||
</sl-menu-item>
|
||||
${this.egw.user('apps')['admin'] !== undefined ? html`
|
||||
<sl-menu-item
|
||||
@click=${(e) =>
|
||||
{
|
||||
// @ts-ignore
|
||||
egw_link_handler(`/egroupware/index.php?menuaction=admin.admin_ui.index&load=admin.uiconfig.index&appname=${this.name}&ajax=true`, 'admin');
|
||||
}}
|
||||
>
|
||||
<sl-icon slot="prefix" name="gear-wide"></sl-icon>
|
||||
${this.egw.lang("App configuration")}
|
||||
</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
` : nothing}
|
||||
${this._sideboxMenuTemplate()}
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</sl-tooltip>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _sideboxMenuTemplate()
|
||||
{
|
||||
if(!this._sideboxData)
|
||||
{
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${repeat(this._sideboxData, (menu) => menu['menu_name'], (menu) =>
|
||||
{
|
||||
// No favorites here
|
||||
if(menu["title"] == "Favorites" || menu["title"] == this.egw.lang("favorites"))
|
||||
{
|
||||
return html`
|
||||
<sl-menu-item>
|
||||
<et2-image style="width:1em;" src="fav_filter" slot="prefix"></et2-image>
|
||||
${menu["title"]}
|
||||
<et2-favorites-menu slot="submenu" application="${this.appName}"></et2-favorites-menu>
|
||||
</sl-menu-item>
|
||||
`;
|
||||
}
|
||||
// Just one thing, don't bother with submenu
|
||||
if(menu["entries"].length == 1)
|
||||
{
|
||||
return this._sideboxMenuItemTemplate({...menu["entries"][0], lang_item: menu["title"]})
|
||||
}
|
||||
return html`
|
||||
<sl-menu-item>
|
||||
${menu["title"]}
|
||||
<sl-menu slot="submenu">
|
||||
${repeat(menu["entries"], (entry) =>
|
||||
{
|
||||
return this._sideboxMenuItemTemplate(entry);
|
||||
})}
|
||||
</sl-menu>
|
||||
</sl-menu-item>`;
|
||||
})}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* An individual sub-item in the 3-dots menu
|
||||
* @param item
|
||||
* @returns {TemplateResult<1>}
|
||||
*/
|
||||
_sideboxMenuItemTemplate(item)
|
||||
{
|
||||
if(item["lang_item"] == "<hr />")
|
||||
{
|
||||
return html`
|
||||
<sl-divider></sl-divider>`;
|
||||
}
|
||||
return html`
|
||||
<sl-menu-item
|
||||
?disabled=${!item["item_link"]}
|
||||
@click=${() => this.egw.open_link(item["item_link"])}
|
||||
>
|
||||
${typeof item["icon_or_star"] == "string" && item["icon_or_star"].endsWith("bullet.svg") ? nothing : html`
|
||||
<et2-image name=${item["icon_or_star"]}></et2-image>
|
||||
`}
|
||||
${item["lang_item"]}
|
||||
</sl-menu-item>`;
|
||||
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
const hasLeftSlots = this.hasSideContent("left");
|
||||
@ -552,12 +658,13 @@ export class EgwFrameworkApp extends LitElement
|
||||
></sl-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
<h2>${this.egw?.lang(this.name) ?? this.name}</h2>
|
||||
<h2>${this.title || this.egw?.lang(this.name) || this.name}</h2>
|
||||
</div>
|
||||
<header class="egw_fw_app__header" part="header">
|
||||
<slot name="main-header"><span class="placeholder"> ${this.name} main-header</span></slot>
|
||||
</header>
|
||||
${this._rightHeaderTemplate()}
|
||||
${until(this.framework.getEgwComplete().then(() => this._rightHeaderTemplate()), html`
|
||||
<sl-spinner></sl-spinner>`)}
|
||||
</div>
|
||||
<div class="egw_fw_app__main" part="main">
|
||||
<sl-split-panel class=${classMap({"egw_fw_app__outerSplit": true, "no-content": !hasLeftSlots})}
|
||||
|
@ -19,16 +19,11 @@ document.addEventListener('DOMContentLoaded', () =>
|
||||
}
|
||||
/* Set up listener on avatar menu */
|
||||
const avatarMenu = document.querySelector("#topmenu_info_user_avatar");
|
||||
avatarMenu.addEventListener("sl-select", (e : CustomEvent) =>
|
||||
if(avatarMenu)
|
||||
{
|
||||
window.egw.open_link(e.detail.item.value);
|
||||
});
|
||||
|
||||
/* Listener on placeholder checkbox */
|
||||
// TODO: Remove this & the switch
|
||||
document.querySelector("#placeholders").addEventListener("sl-change", (e) =>
|
||||
{
|
||||
document.querySelector("egw-framework").classList.toggle("placeholder", e.target.checked);
|
||||
document.querySelector("egw-app").classList.toggle("placeholder", e.target.checked);
|
||||
});
|
||||
avatarMenu.addEventListener("sl-select", (e : CustomEvent) =>
|
||||
{
|
||||
window.egw.open_link(e.detail.item.value);
|
||||
});
|
||||
}
|
||||
});
|
32
package-lock.json
generated
32
package-lock.json
generated
@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@bundled-es-modules/pdfjs-dist": "^2.5.207-rc1",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@shoelace-style/shoelace": "2.15.0",
|
||||
"@shoelace-style/shoelace": "2.15.1",
|
||||
"@types/jquery": "^3.5.29",
|
||||
"blueimp-gallery": "^3.4.0",
|
||||
"colortranslator": "^1.9.2",
|
||||
@ -3409,9 +3409,9 @@
|
||||
"integrity": "sha512-Hf45HeO+vdQblabpyZOTxJ4ZeZsmIUYXXPmoYrrR4OJ5OKxL+bhMz5mK8JXgl7HsoEowfz7+e248UGi861de9Q=="
|
||||
},
|
||||
"node_modules/@shoelace-style/shoelace": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@shoelace-style/shoelace/-/shoelace-2.15.0.tgz",
|
||||
"integrity": "sha512-Lcg938Y8U2VsHqIYewzlt+H1rbrXC4GRSUkTJgXyF8/0YAOlI+srd5OSfIw+/LYmwLP2Peyh398Kae/6tg4PDA==",
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@shoelace-style/shoelace/-/shoelace-2.15.1.tgz",
|
||||
"integrity": "sha512-3ecUw8gRwOtcZQ8kWWkjk4FTfObYQ/XIl3aRhxprESoOYV1cYhloYPsmQY38UoL3+pwJiZb5+LzX0l3u3Zl0GA==",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^4.0.2",
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
@ -3857,6 +3857,12 @@
|
||||
"integrity": "sha512-ARATsLdrGPUnaBvxLhUlnltcMgn7pQG312S8ccdYlnyijabrX9RN/KN/iGj9Am96CoW8e/K9628BA7Bv4XHdrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
@ -3869,6 +3875,16 @@
|
||||
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
|
||||
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
@ -7089,6 +7105,12 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/custom-element-jet-brains-integration": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/custom-element-jet-brains-integration/-/custom-element-jet-brains-integration-1.2.1.tgz",
|
||||
@ -12940,7 +12962,7 @@
|
||||
"version": "2.79.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
||||
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
@ -83,7 +83,7 @@
|
||||
"dependencies": {
|
||||
"@bundled-es-modules/pdfjs-dist": "^2.5.207-rc1",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@shoelace-style/shoelace": "2.15.0",
|
||||
"@shoelace-style/shoelace": "2.15.1",
|
||||
"@types/jquery": "^3.5.29",
|
||||
"blueimp-gallery": "^3.4.0",
|
||||
"colortranslator": "^1.9.2",
|
||||
|
@ -27,7 +27,6 @@
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @author Stefan Reinhard <stefan.reinhard@pixelegg.de>
|
||||
* @package pixelegg
|
||||
* @version $Id$
|
||||
*/
|
||||
/**
|
||||
* addapted from orginal styles.php
|
||||
@ -2198,6 +2197,7 @@ div#loginMainDiv.stockLoginBackground div#centerBox form {
|
||||
border-bottom: 1px solid silver;
|
||||
padding-left: 25px;
|
||||
background-color: transparent;
|
||||
font-size: 100%;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox input:focus {
|
||||
outline: none;
|
||||
|
@ -7,7 +7,6 @@
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @author Stefan Reinhard <stefan.reinhard@pixelegg.de>
|
||||
* @package pixelegg
|
||||
* @version $Id$
|
||||
*/
|
||||
/**
|
||||
* addapted from orginal styles.php
|
||||
@ -2178,6 +2177,7 @@ div#loginMainDiv.stockLoginBackground div#centerBox form {
|
||||
border-bottom: 1px solid silver;
|
||||
padding-left: 25px;
|
||||
background-color: transparent;
|
||||
font-size: 100%;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox input:focus {
|
||||
outline: none;
|
||||
|
@ -7,7 +7,6 @@
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @author Stefan Reinhard <stefan.reinhard@pixelegg.de>
|
||||
* @package pixelegg
|
||||
* @version $Id$
|
||||
*/
|
||||
/**
|
||||
* addapted from orginal styles.php
|
||||
|
@ -17,7 +17,6 @@
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @author Stefan Reinhard <stefan.reinhard@pixelegg.de>
|
||||
* @package pixelegg
|
||||
* @version $Id$
|
||||
*/
|
||||
/**
|
||||
* addapted from orginal styles.php
|
||||
@ -2188,6 +2187,7 @@ div#loginMainDiv.stockLoginBackground div#centerBox form {
|
||||
border-bottom: 1px solid silver;
|
||||
padding-left: 25px;
|
||||
background-color: transparent;
|
||||
font-size: 100%;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox input:focus {
|
||||
outline: none;
|
||||
|
@ -341,6 +341,7 @@ div#loginMainDiv.stockLoginBackground {
|
||||
border-bottom: 1px solid silver;
|
||||
padding-left: 25px;
|
||||
background-color: transparent;
|
||||
font-size: 100%;
|
||||
}
|
||||
input:hover {}
|
||||
input:focus {
|
||||
|
@ -188,7 +188,6 @@
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @author Stefan Reinhard <stefan.reinhard@pixelegg.de>
|
||||
* @package pixelegg
|
||||
* @version $Id$
|
||||
*/
|
||||
/**
|
||||
* addapted from orginal styles.php
|
||||
@ -2209,6 +2208,7 @@ div#loginMainDiv.stockLoginBackground div#centerBox form {
|
||||
border-bottom: 1px solid silver;
|
||||
padding-left: 25px;
|
||||
background-color: transparent;
|
||||
font-size: 100%;
|
||||
}
|
||||
#loginMainDiv div#centerBox form table.divLoginbox input:focus {
|
||||
outline: none;
|
||||
|
Loading…
Reference in New Issue
Block a user