From c6ef3b3a8dad3241d542f50414ececb87fa6da05 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 5 Jul 2022 10:18:12 -0600 Subject: [PATCH] Switch calendar owner to web component --- ...ss.calendar_owner_etemplate_widget.inc.php | 62 +++---- calendar/js/CalendarOwner.ts | 92 +++++++++++ calendar/js/app.ts | 63 ++++---- calendar/js/et2_widget_event.ts | 10 +- calendar/js/et2_widget_owner.ts | 152 ------------------ calendar/js/et2_widget_planner.ts | 12 +- calendar/js/et2_widget_view.ts | 4 +- calendar/templates/default/app.css | 11 -- calendar/templates/default/edit.xet | 19 ++- calendar/templates/mobile/app.css | 11 -- calendar/templates/pixelegg/app.css | 11 -- 11 files changed, 179 insertions(+), 268 deletions(-) create mode 100644 calendar/js/CalendarOwner.ts delete mode 100644 calendar/js/et2_widget_owner.ts diff --git a/calendar/inc/class.calendar_owner_etemplate_widget.inc.php b/calendar/inc/class.calendar_owner_etemplate_widget.inc.php index 6d503c6c92..65c3561dd7 100644 --- a/calendar/inc/class.calendar_owner_etemplate_widget.inc.php +++ b/calendar/inc/class.calendar_owner_etemplate_widget.inc.php @@ -148,25 +148,31 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist Api\Json\Response::get()->data($label); return $label; } - else if($id && is_array($id)) + else { - $labels = Array(); - foreach($id as $index => $_id) + if($id && is_array($id)) { - $labels[$_id] = self::format_owner($_id, self::get_owner_label($_id)); + $labels = array(); + foreach($id as $index => $_id) + { + $labels[$_id] = self::format_owner($_id, self::get_owner_label($_id)); + } + Api\Json\Response::get()->data($labels); + return $labels; } - Api\Json\Response::get()->data($labels); - return $labels; } + } + + public static function ajax_search($search_text, $search_options = []) + { $bo = new calendar_bo(); - $query = $_REQUEST['query']; // Arbitrarily limited to 50 / resource - $options = array('start' => 0, 'num_rows' => 50, - // Filter accounts out of addressbook - 'filter' => array('account_id' => null)) + - array_diff_key($_REQUEST, array_flip(array('menuaction','query'))); + $options = array('start' => 0, 'num_rows' => 50, + // Filter accounts out of addressbook + 'filter' => array('account_id' => null)) + + $search_options; $results = array(); // Contacts matching accounts the user does not have permission for cause @@ -174,7 +180,7 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist // we remove those contacts $remove_contacts = array(); - $resources = array_merge(array('' => $bo->resources['']),$bo->resources); + $resources = array_merge(array('' => $bo->resources['']), $bo->resources); $contacts_obj = new Api\Contacts(); foreach($resources as $type => $data) { @@ -187,7 +193,7 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist $owngroup_options = $options+array('account_type'=>'owngroups'); $own_groups = Api\Accounts::link_query('',$owngroup_options); $account_options = $options + array('account_type' => 'both'); - $_results += $remove_contacts = Api\Accounts::link_query($query, $account_options); + $_results += $remove_contacts = Api\Accounts::link_query($search_text, $account_options); if (!empty($_REQUEST['checkgrants'])) { $grants = (array)$GLOBALS['egw']->acl->get_grants('calendar') + $own_groups; @@ -197,12 +203,12 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist // App provides a custom search function else if ($data['app'] && $data['search']) { - $_results = call_user_func_array($data['search'], array($query, $options)); + $_results = call_user_func_array($data['search'], array($search_text, $options)); } // Use standard link registry else if ($data['app'] && Link::get_registry($data['app'], 'query')) { - $_results = Link::query($data['app'], $query,$options); + $_results = Link::query($data['app'], $search_text, $options); } // There are always special cases @@ -212,10 +218,11 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist // Include mailing lists, but not account groups $lists = array_filter( $contacts_obj->get_lists(Api\Acl::READ), - function($element, $index) use($query) { - return $index > 0 && (stripos($element, $query) !== false); + function ($element, $index) use ($search_text) + { + return $index > 0 && (stripos($element, $search_text) !== false); }, - ARRAY_FILTER_USE_BOTH + ARRAY_FILTER_USE_BOTH ); foreach($lists as $list_id => $list) { @@ -244,12 +251,7 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist } } - // switch regular JSON response handling off - Api\Json\Request::isJSONRequest(false); - - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($results); - exit(); + Api\Json\Response::get()->data($results); } /** @@ -280,12 +282,10 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist } $type = $data['type']; - // Magicsuggest uses id, not value. $value = array( - 'id' => $type.$id, - 'value'=> $type.$id, + 'value' => $id, 'label' => $title, - 'app' => lang($data['app']) + 'app' => lang($data['app']) ); if(is_array($value['label'])) { @@ -338,8 +338,10 @@ class calendar_owner_etemplate_widget extends Etemplate\Widget\Taglist } else { - $label = Link::title('api-accounts',$id) ?: Api\Accounts::username($id); + $label = Link::title('api-accounts', $id) ?: Api\Accounts::username($id); } return $label; } -} \ No newline at end of file +} + +Etemplate\Widget::registerWidget(__NAMESPACE__ . '\\calendar_owner_etemplate_widget', array('calendar-owner')); \ No newline at end of file diff --git a/calendar/js/CalendarOwner.ts b/calendar/js/CalendarOwner.ts new file mode 100644 index 0000000000..0aaf28b2d1 --- /dev/null +++ b/calendar/js/CalendarOwner.ts @@ -0,0 +1,92 @@ +/* + * Calendar owner widget + * + * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package calendar + * @subpackage etemplate + * @link https://www.egroupware.org + * @author Nathan Gray + */ + +import {Et2Select} from "../../api/js/etemplate/Et2Select/Et2Select"; +import {css} from "@lion/core"; + +/** + * Select widget customised for calendar owner, which can be a user + * account or group, or an entry from almost any app, or an email address + * + */ +export class CalendarOwner extends Et2Select +{ + + static get styles() + { + return [ + ...super.styles, + css` + /* Larger maximum height before scroll*/ + .select__tags { + max-height: 10em; + } + ` + ]; + } + + constructor(...args : any[]) + { + super(...args); + this.searchUrl = "calendar_owner_etemplate_widget::ajax_search"; + this.multiple = true; + } + + /** + * Override parent to handle our special additional data types (c#,r#,etc.) when they + * are not available client side. + * + * @param {string|string[]} _value array of selected owners, which can be a number, + * or a number prefixed with one character indicating the resource type. + */ + set_value(_value) + { + super.set_value(_value); + + // If parent didn't find a label, label will be the same as ID so we + // can find them that way + let missing_labels = []; + for(var i = 0; i < this.value.length; i++) + { + if(!this.menuItems.find(o => o.value == this.value[i])) + { + missing_labels.push(this.value[i]); + } + } + if(Object.keys(missing_labels).length > 0) + { + // Proper label was not found by parent - ask directly + this.egw().json('calendar_owner_etemplate_widget::ajax_owner', [missing_labels], function(data) + { + for(let owner in data) + { + if(!owner || typeof owner == "undefined") + { + continue; + } + // Put it in the list of options + let index = this.select_options.findIndex(o => o.value == owner); + if(index !== -1) + { + this.select_options[index] = data[owner]; + } + else + { + this.select_options.push(data[owner]); + } + } + this.requestUpdate("select_options"); + this.updateComplete.then(() => {this.syncItemsFromValue();}); + }, this, true, this).sendRequest(); + } + } +} + +customElements.define("calendar-owner", CalendarOwner); \ No newline at end of file diff --git a/calendar/js/app.ts b/calendar/js/app.ts index b132a7fb68..76a490bd35 100644 --- a/calendar/js/app.ts +++ b/calendar/js/app.ts @@ -23,7 +23,6 @@ import {EgwApp, PushData} from "../../api/js/jsapi/egw_app"; import {etemplate2} from "../../api/js/etemplate/etemplate2"; import {et2_container} from "../../api/js/etemplate/et2_core_baseWidget"; import {et2_date} from "../../api/js/etemplate/et2_widget_date"; -import {et2_calendar_owner} from "./et2_widget_owner"; import {day, day4, listview, month, planner, week, weekN} from "./View"; import {et2_calendar_view} from "./et2_widget_view"; import {et2_calendar_timegrid} from "./et2_widget_timegrid"; @@ -56,6 +55,7 @@ import {nm_action} from "../../api/js/etemplate/et2_extension_nextmatch_actions" import flatpickr from "flatpickr"; import Sortable from 'sortablejs/modular/sortable.complete.esm.js'; import {tapAndSwipe} from "../../api/js/tapandswipe"; +import {CalendarOwner} from "./CalendarOwner"; /** * UI for calendar @@ -263,7 +263,7 @@ export class CalendarApp extends EgwApp if(sidebox.length == 0 && egw_getFramework() != null) { // Force rollup to load owner widget, it leaves it out otherwise - new et2_calendar_owner(_et2.widgetContainer, {}); + new CalendarOwner(); // Force rollup to load planner widget, it leaves it out otherwise new et2_calendar_planner(_et2.widgetContainer, {}); @@ -1482,17 +1482,25 @@ export class CalendarApp extends EgwApp */ edit_update_participant(input, widget?) { - if(typeof widget === 'undefined') widget = input; + if(typeof widget === 'undefined') + { + widget = input; + } var content = widget.getInstanceManager().getValues(widget.getRoot()); - var participant = widget.getRoot().getWidgetById('participant'); - if(!participant) return; + var participant = widget.getRoot().getWidgetById('participant'); + if(!participant) + { + return; + } - participant.set_autocomplete_params({exec:{ - start: content.start, - end: content.end, - duration: content.duration, - whole_day: content.whole_day, - }}); + participant.searchOptions = { + exec: { + start: content.start, + end: content.end, + duration: content.duration, + whole_day: content.whole_day, + } + }; } /** @@ -1618,12 +1626,12 @@ export class CalendarApp extends EgwApp */ participantOnChange() { - var add = this.et2.getWidgetById('add'); - var quantity = this.et2.getWidgetById('quantity'); - var participant = this.et2.getWidgetById('participant'); + var add = this.et2.getWidgetById('add'); + var quantity = this.et2.getWidgetById('quantity'); + var participant = this.et2.getWidgetById('participant'); // array of participants - var value = participant.get_value(); + let value = participant.value; add.set_readonly(value.length <= 0); @@ -3700,27 +3708,27 @@ export class CalendarApp extends EgwApp { var found = false; var option = data.rows.sel_options[field][i]; - for(var j in widget.options.select_options) + for(var j in widget.select_options) { - if(option.value == widget.options.select_options[j].value) + if(option.value == widget.select_options[j].value) { - widget.options.select_options[j].label = option.label; + widget.select_options[j].label = option.label; found = true; break; } } if(!found) { - if(!widget.options.select_options.push) + if(!widget.select_options.push) { - widget.options.select_options = []; + widget.select_options = []; } - widget.options.select_options.push(option); + widget.select_options.push(option); } } var in_progress = app.calendar.state_update_in_progress; app.calendar.state_update_in_progress = true; - widget.set_select_options(widget.options.select_options); + widget.set_select_options(widget.select_options); widget.set_value(widget.getValue()); app.calendar.state_update_in_progress = in_progress; @@ -4173,17 +4181,6 @@ export class CalendarApp extends EgwApp button.parent().css('margin-right',button.outerWidth(true)+2); button.parent().parent().css('white-space','nowrap'); } - jQuery(window).on('resize.calendar-owner', function() { - var preferred_width = jQuery('#calendar-et2_target').children().first().outerWidth()||0; - if(app.calendar && app.calendar.sidebox_et2) - { - var owner = app.calendar.sidebox_et2.getWidgetById('owner'); - if(preferred_width && owner.input.hasClass("chzn-done")) - { - owner.input.next().css('width',preferred_width); - } - } - }); } /** diff --git a/calendar/js/et2_widget_event.ts b/calendar/js/et2_widget_event.ts index e8489d27c4..2d29ddf734 100644 --- a/calendar/js/et2_widget_event.ts +++ b/calendar/js/et2_widget_event.ts @@ -961,13 +961,13 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached let options: any = null; if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); + options = app.calendar.sidebox_et2.getWidgetById('owner').select_options } if ((isNaN(parseInt(owner)) || parseInt(owner) < 0) && options && typeof options.find == "function") { let resource = options.find(function (element) { - return element.id == owner; + return element.value == owner; }) || {}; let matching_participant = typeof resource.resources == "undefined" ? resource : @@ -1201,7 +1201,7 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached let options: any = null; if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); + options = app.calendar.sidebox_et2.getWidgetById('owner').select_options; } else { @@ -1227,7 +1227,7 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached { var resource = options.find(function(element) { - return element.id == parent_owner[i]; + return element.value == parent_owner[i]; }) || {}; if(resource && resource.resources) { @@ -1249,7 +1249,7 @@ export class et2_calendar_event extends et2_valueWidget implements et2_IDetached var resource; if (options && options.find && (resource = options.find(function (element) { - return element.id === id; + return element.value === id; })) && resource.resources) { participants = participants.concat(resource.resources); diff --git a/calendar/js/et2_widget_owner.ts b/calendar/js/et2_widget_owner.ts deleted file mode 100644 index 16b624e0e1..0000000000 --- a/calendar/js/et2_widget_owner.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Egroupware - * - * @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @package calendar - * @subpackage etemplate - * @link https://www.egroupware.org - * @author Nathan Gray - */ - -import {et2_register_widget} from "../../api/js/etemplate/et2_core_widget.ts"; -import {et2_taglist_email} from "../../api/js/etemplate/et2_widget_taglist"; - -/** - * Tag list widget customised for calendar owner, which can be a user - * account or group, or an entry from almost any app, or an email address - * - * A cross between auto complete, selectbox and chosen multiselect - * - * Uses MagicSuggest library - * @see http://nicolasbize.github.io/magicsuggest/ - * @augments et2_taglist_email - */ -export class et2_calendar_owner extends et2_taglist_email -{ - static readonly _attributes = { - "autocomplete_url": { - "default": "calendar_owner_etemplate_widget::ajax_owner" - }, - "autocomplete_params": { - "name": "Autocomplete parameters", - "type": "any", - "default": {}, - "description": "Extra parameters passed to autocomplete URL. It should be a stringified JSON object." - }, - allowFreeEntries: { - "default": false, - ignore: true - }, - select_options: { - "type": "any", - "name": "Select options", - // Set to empty object to use selectbox's option finding - "default": {}, - "description": "Internally used to hold the select options." - } - }; - - // Allows sub-widgets to override options to the library - lib_options = { - autoSelect: false, - groupBy: 'app', - minChars: 2, - selectFirst: true, - // This option will also expand when the selection is changed - // via code, which we do not want - //expandOnFocus: true - toggleOnClick: true - }; - - doLoadingFinished() - { - super.doLoadingFinished(); - - var widget = this; - // onChange fired when losing focus, which is different from normal - this._oldValue = this.taglist.getValue(); - - return true; - } - - selectionRenderer(item) - { - if(this && this.options && this.options.allowFreeEntries) - { - return super.selectionRenderer(item); - } - else - { - var label = jQuery('').text(item.label); - if (item.class) label.addClass(item.class); - if (typeof item.title != 'undefined') label.attr('title', item.title); - if (typeof item.data != 'undefined') label.attr('data', item.data); - if (typeof item.icon != 'undefined') - { - var wrapper = jQuery('
').addClass('et2_taglist_tags_icon_wrapper'); - jQuery('') - .addClass('et2_taglist_tags_icon') - .css({"background-image": "url("+(item.icon.match(/^(http|https|\/)/) ? item.icon : egw.image(item.icon, item.app))+")"}) - .appendTo(wrapper); - label.appendTo(wrapper); - return wrapper; - } - return label; - } - } - - getValue() - { - if(this.taglist == null) return null; - return this.taglist.getValue(); - } - - /** - * Override parent to handle our special additional data types (c#,r#,etc.) when they - * are not available client side. - * - * @param {string|string[]} _value array of selected owners, which can be a number, - * or a number prefixed with one character indicating the resource type. - */ - set_value(_value) - { - super.set_value(_value); - - // If parent didn't find a label, label will be the same as ID so we - // can find them that way - let missing_labels = []; - for(var i = 0; i < this.options.value.length; i++) - { - var value = this.options.value[i]; - if(value.id == value.label) - { - missing_labels.push(value.id); - } - } - if(Object.keys(missing_labels).length > 0) - { - // Proper label was not found by parent - ask directly - egw.json('calendar_owner_etemplate_widget::ajax_owner',[missing_labels],function(data) { - for(let owner in data) - { - if(!owner || typeof owner == "undefined") continue; - let idx = this.options.value.find(element => element.id == owner); - if(idx) - { - idx = jQuery.extend(idx, data[owner]); - } - // Put it in the list of options for next time - this.options.select_options.push(data[owner]); - } - this.set_value(this.options.value); - }, this,true,this).sendRequest(); - } - - if(this.taglist) - { - this.taglist.clear(true); - this.taglist.addToSelection(this.options.value,true); - } - } -} -et2_register_widget(et2_calendar_owner, ["calendar-owner"]); \ No newline at end of file diff --git a/calendar/js/et2_widget_planner.ts b/calendar/js/et2_widget_planner.ts index dbf73baf75..23bc8fd86c 100644 --- a/calendar/js/et2_widget_planner.ts +++ b/calendar/js/et2_widget_planner.ts @@ -408,11 +408,11 @@ export class et2_calendar_planner extends et2_calendar_view implements et2_IDeta row_labels: function() { var labels = []; var already_added = []; - var options = false; + var options = []; var resource = null; if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); + options = app.calendar.sidebox_et2.getWidgetById('owner').select_options; } else { @@ -424,14 +424,14 @@ export class et2_calendar_planner extends et2_calendar_view implements et2_IDeta // Handle grouped resources like mailing lists - pull it from sidebox owner // and expand to their contents if(options && options.find && - ((resource = options.find(function(element) {return element.id == user;}) || {}) || isNaN(user))) + ((resource = options.find(function(element) {return element.value == user;}) || {}) || isNaN(user))) { if(resource && resource.resources) { for(var j = 0; j < resource.resources.length; j++) { var id = resource.resources[j]; - if(already_added.indexOf(''+id) < 0) + if(already_added.indexOf('' + id) < 0) { labels.push({ id: id, @@ -2128,7 +2128,7 @@ export class et2_calendar_planner extends et2_calendar_view implements et2_IDeta if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); + options = app.calendar.sidebox_et2.getWidgetById('owner').select_options; } else { @@ -2143,7 +2143,7 @@ export class et2_calendar_planner extends et2_calendar_view implements et2_IDeta if(options.find && ((resource = options.find(function (element) { - return element.id == user; + return element.value == user; })))) { // Members found diff --git a/calendar/js/et2_widget_view.ts b/calendar/js/et2_widget_view.ts index 1548cd31b2..e65f98da4f 100644 --- a/calendar/js/et2_widget_view.ts +++ b/calendar/js/et2_widget_view.ts @@ -440,7 +440,7 @@ export class et2_calendar_view extends et2_valueWidget let options = false; if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner')) { - options = app.calendar.sidebox_et2.getWidgetById('owner').taglist.getSelection(); + options = app.calendar.sidebox_et2.getWidgetById('owner').select_options; } else { @@ -448,7 +448,7 @@ export class et2_calendar_view extends et2_valueWidget } if(options && options.find) { - var found = options.find(function(element) {return element.id == user;}) || {}; + var found = options.find(function(element) {return element.value == user;}) || {}; if(found && found.label && found.label !== user) { label = found.label; diff --git a/calendar/templates/default/app.css b/calendar/templates/default/app.css index fca5a543a5..8e57e6e733 100644 --- a/calendar/templates/default/app.css +++ b/calendar/templates/default/app.css @@ -70,13 +70,6 @@ #calendar-et2_target > div { position: relative; } -#calendar-sidebox_owner { - width: 90%; -} -#calendar-sidebox_owner ~ * { - vertical-align: top; - display: inline-block; -} #calendar-sidebox_cat_id { margin-bottom: 10px; } @@ -120,10 +113,6 @@ #calendar-sidebox_date .calendar_calHoliday { background-color: rgba(103, 159, 210, 0.5); } -#calendar-sidebox_owner .ms-helper { - padding: 2px; - background-color: white; -} #calendar-sidebox_date .flatpickr-months { gap: 0; diff --git a/calendar/templates/default/edit.xet b/calendar/templates/default/edit.xet index 72f04626d0..4bfd9f2925 100644 --- a/calendar/templates/default/edit.xet +++ b/calendar/templates/default/edit.xet @@ -57,13 +57,18 @@ -