- Store relation to message when creating event from email (#4161)
- Move common functions to libcalendaring - Assign dialog button classes directly
This commit is contained in:
parent
0a51ccd5a4
commit
b02e2c3b8f
11 changed files with 270 additions and 66 deletions
|
@ -1636,6 +1636,15 @@ class calendar extends rcube_plugin
|
|||
$event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
|
||||
}
|
||||
|
||||
// convert link URIs references into structs
|
||||
if (array_key_exists('links', $event)) {
|
||||
foreach ((array)$event['links'] as $i => $link) {
|
||||
if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link))) {
|
||||
$event['links'][$i] = $msgref;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check for organizer in attendees list
|
||||
$organizer = null;
|
||||
foreach ((array)$event['attendees'] as $i => $attendee) {
|
||||
|
@ -1824,6 +1833,13 @@ class calendar extends rcube_plugin
|
|||
|
||||
$event['attachments'] = $attachments;
|
||||
|
||||
// convert link references into simple URIs
|
||||
if (array_key_exists('links', $event)) {
|
||||
$event['links'] = array_map(function($link) {
|
||||
return is_array($link) ? $link['uri'] : strval($link);
|
||||
}, (array)$event['links']);
|
||||
}
|
||||
|
||||
// check for organizer in attendees
|
||||
if ($action == 'new' || $action == 'edit') {
|
||||
if (!$event['attendees'])
|
||||
|
@ -2932,9 +2948,15 @@ class calendar extends rcube_plugin
|
|||
if ($message->headers) {
|
||||
$event['title'] = trim($message->subject);
|
||||
$event['description'] = trim($message->first_text_part());
|
||||
|
||||
|
||||
$this->load_driver();
|
||||
|
||||
// add a reference to the email message
|
||||
if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
|
||||
$event['links'] = array($msgref);
|
||||
}
|
||||
// copy mail attachments to event
|
||||
if ($message->attachments) {
|
||||
else if ($message->attachments) {
|
||||
$eventid = 'cal:';
|
||||
if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
|
||||
$_SESSION[self::SESSION_KEY] = array();
|
||||
|
|
|
@ -187,6 +187,7 @@ function rcube_calendar_ui(settings)
|
|||
var fromunixtime = this.fromunixtime;
|
||||
var parseISO8601 = this.parseISO8601;
|
||||
var date2servertime = this.date2ISO8601;
|
||||
var render_message_links = this.render_message_links;
|
||||
|
||||
|
||||
/*** private methods ***/
|
||||
|
@ -471,6 +472,13 @@ function rcube_calendar_ui(settings)
|
|||
// fetch attachments, some drivers doesn't set 'attachments' prop of the event?
|
||||
}
|
||||
|
||||
// build attachments list
|
||||
$('#event-links').hide();
|
||||
if ($.isArray(event.links) && event.links.length) {
|
||||
render_message_links(event.links || [], $('#event-links').children('.event-text'), false, 'calendar');
|
||||
$('#event-links').show();
|
||||
}
|
||||
|
||||
// list event attendees
|
||||
if (calendar.attendees && event.attendees) {
|
||||
// sort resources to the end
|
||||
|
@ -546,20 +554,30 @@ function rcube_calendar_ui(settings)
|
|||
$('#event-rsvp .itip-reply-comment textarea').hide().val('');
|
||||
}
|
||||
|
||||
var buttons = {};
|
||||
var buttons = [];
|
||||
if (!temp && calendar.editable && event.editable !== false) {
|
||||
buttons[rcmail.gettext('edit', 'calendar')] = function() {
|
||||
event_edit_dialog('edit', event);
|
||||
};
|
||||
buttons[rcmail.gettext('delete', 'calendar')] = function() {
|
||||
me.delete_event(event);
|
||||
$dialog.dialog('close');
|
||||
};
|
||||
buttons.push({
|
||||
text: rcmail.gettext('edit', 'calendar'),
|
||||
click: function() {
|
||||
event_edit_dialog('edit', event);
|
||||
}
|
||||
});
|
||||
buttons.push({
|
||||
text: rcmail.gettext('delete', 'calendar'),
|
||||
'class': 'delete',
|
||||
click: function() {
|
||||
me.delete_event(event);
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
buttons[rcmail.gettext('close', 'calendar')] = function(){
|
||||
$dialog.dialog('close');
|
||||
};
|
||||
buttons.push({
|
||||
text: rcmail.gettext('close', 'calendar'),
|
||||
click: function(){
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// open jquery UI dialog
|
||||
|
@ -712,6 +730,14 @@ function rcube_calendar_ui(settings)
|
|||
$('<option>').attr('value', event.categories).text(event.categories).appendTo(categories).prop('selected', true);
|
||||
}
|
||||
|
||||
if ($.isArray(event.links) && event.links.length) {
|
||||
render_message_links(event.links, $('#edit-event-links .event-text'), true, 'calendar');
|
||||
$('#edit-event-links').show();
|
||||
}
|
||||
else {
|
||||
$('#edit-event-links').hide();
|
||||
}
|
||||
|
||||
// show warning if editing a recurring event
|
||||
if (event.id && event.recurrence) {
|
||||
var sel = event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all');
|
||||
|
@ -778,10 +804,13 @@ function rcube_calendar_ui(settings)
|
|||
};
|
||||
|
||||
// init dialog buttons
|
||||
var buttons = {};
|
||||
var buttons = [];
|
||||
|
||||
// save action
|
||||
buttons[rcmail.gettext('save', 'calendar')] = function() {
|
||||
buttons.push({
|
||||
text: rcmail.gettext('save', 'calendar'),
|
||||
'class': 'mainaction',
|
||||
click: function() {
|
||||
var start = parse_datetime(allday.checked ? '12:00' : starttime.val(), startdate.val());
|
||||
var end = parse_datetime(allday.checked ? '13:00' : endtime.val(), enddate.val());
|
||||
|
||||
|
@ -809,6 +838,7 @@ function rcube_calendar_ui(settings)
|
|||
recurrence: me.serialize_recurrence(endtime.val()),
|
||||
valarms: me.serialize_alarms('#edit-alarms'),
|
||||
attendees: event_attendees,
|
||||
links: me.selected_event.links,
|
||||
deleted_attachments: rcmail.env.deleted_attachments,
|
||||
attachments: []
|
||||
};
|
||||
|
@ -865,18 +895,26 @@ function rcube_calendar_ui(settings)
|
|||
|
||||
update_event(action, data);
|
||||
$dialog.dialog("close");
|
||||
};
|
||||
} // end click:
|
||||
});
|
||||
|
||||
if (event.id) {
|
||||
buttons[rcmail.gettext('delete', 'calendar')] = function() {
|
||||
me.delete_event(event);
|
||||
$dialog.dialog('close');
|
||||
};
|
||||
buttons.push({
|
||||
text: rcmail.gettext('delete', 'calendar'),
|
||||
'class': 'delete',
|
||||
click: function() {
|
||||
me.delete_event(event);
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
|
||||
$dialog.dialog("close");
|
||||
};
|
||||
buttons.push({
|
||||
text: rcmail.gettext('cancel', 'calendar'),
|
||||
click: function() {
|
||||
$dialog.dialog("close");
|
||||
}
|
||||
});
|
||||
|
||||
// show/hide tabs according to calendar's feature support
|
||||
$('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
|
||||
|
@ -900,7 +938,6 @@ function rcube_calendar_ui(settings)
|
|||
title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
|
||||
open: function() {
|
||||
editform.attr('aria-hidden', 'false');
|
||||
$dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
|
||||
},
|
||||
close: function() {
|
||||
editform.hide().attr('aria-hidden', 'true').appendTo(document.body);
|
||||
|
@ -2414,7 +2451,18 @@ function rcube_calendar_ui(settings)
|
|||
})
|
||||
$.each(listitems, function(idx, item) { mylist.append(item); });
|
||||
}
|
||||
|
||||
|
||||
// remove the link reference matching the given uri
|
||||
function remove_link(elem)
|
||||
{
|
||||
var $elem = $(elem), uri = $elem.attr('data-uri');
|
||||
|
||||
me.selected_event.links = $.grep(me.selected_event.links, function(link) { return link.uri != uri; });
|
||||
|
||||
// remove UI list item
|
||||
$elem.hide().closest('li').addClass('deleted');
|
||||
}
|
||||
|
||||
// post the given event data to server
|
||||
var update_event = function(action, data, add)
|
||||
{
|
||||
|
@ -4054,6 +4102,20 @@ function rcube_calendar_ui(settings)
|
|||
return false;
|
||||
})
|
||||
|
||||
// register click handler for message links
|
||||
$('#edit-event-links, #event-links').on('click', 'li a.messagelink', function(e) {
|
||||
rcmail.open_window(this.href);
|
||||
if (!rcube_event.is_keyboard(e) && this.blur)
|
||||
this.blur();
|
||||
return false;
|
||||
});
|
||||
|
||||
// register click handler for message delete buttons
|
||||
$('#edit-event-links').on('click', 'li a.delete', function(e) {
|
||||
remove_link(e.target);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#agenda-listrange').change(function(e){
|
||||
settings['agenda_range'] = parseInt($(this).val());
|
||||
fc.fullCalendar('option', 'listRange', settings['agenda_range']).fullCalendar('render');
|
||||
|
|
|
@ -377,6 +377,21 @@ abstract class calendar_driver
|
|||
*/
|
||||
public function get_attachment_body($id, $event) { }
|
||||
|
||||
/**
|
||||
* Build a struct representing the given message reference
|
||||
*
|
||||
* @param object|string $uri_or_headers rcube_message_header instance holding the message headers
|
||||
* or an URI from a stored link referencing a mail message.
|
||||
* @param string $folder IMAP folder the message resides in
|
||||
*
|
||||
* @return array An struct referencing the given IMAP message
|
||||
*/
|
||||
public function get_message_reference($uri_or_headers, $folder = null)
|
||||
{
|
||||
// to be implemented by the derived classes
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* List availabale categories
|
||||
* The default implementation reads them from config/user prefs
|
||||
|
|
|
@ -391,6 +391,10 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
if (!is_array($event))
|
||||
return false;
|
||||
|
||||
// email links are stored separately
|
||||
$links = $event['links'];
|
||||
unset($event['links']);
|
||||
|
||||
//generate new event from RC input
|
||||
$object = $this->_from_rcube_event($event);
|
||||
$saved = $this->storage->save($object, 'event');
|
||||
|
@ -404,6 +408,9 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
$saved = false;
|
||||
}
|
||||
else {
|
||||
// save links in configuration.relation object
|
||||
$this->save_links($event['uid'], $links);
|
||||
|
||||
$event['id'] = $event['uid'];
|
||||
$this->events = array($event['uid'] => $this->_to_rcube_event($object));
|
||||
}
|
||||
|
@ -425,6 +432,10 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
if (!$old || PEAR::isError($old))
|
||||
return false;
|
||||
|
||||
// email links are stored separately
|
||||
$links = $event['links'];
|
||||
unset($event['links']);
|
||||
|
||||
$object = $this->_from_rcube_event($event, $old);
|
||||
$saved = $this->storage->save($object, 'event', $event['id']);
|
||||
|
||||
|
@ -436,6 +447,9 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
true, false);
|
||||
}
|
||||
else {
|
||||
// save links in configuration.relation object
|
||||
$this->save_links($event['uid'], $links);
|
||||
|
||||
$updated = true;
|
||||
$this->events[$event['id']] = $this->_to_rcube_event($object);
|
||||
|
||||
|
@ -491,6 +505,29 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find messages linked with an event
|
||||
*/
|
||||
protected function get_links($uid)
|
||||
{
|
||||
$storage = kolab_storage_config::get_instance();
|
||||
return $storage->get_object_links($uid);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected function save_links($uid, $links)
|
||||
{
|
||||
// make sure we have a valid array
|
||||
if (empty($links)) {
|
||||
$links = array();
|
||||
}
|
||||
|
||||
$storage = kolab_storage_config::get_instance();
|
||||
$remove = array_diff($storage->get_object_links($uid), $links);
|
||||
return $storage->save_object_links($uid, $links, $remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create instances of a recurring event
|
||||
|
@ -635,6 +672,7 @@ class kolab_calendar extends kolab_storage_folder_api
|
|||
{
|
||||
$record['id'] = $record['uid'];
|
||||
$record['calendar'] = $this->id;
|
||||
$record['links'] = $this->get_links($record['uid']);
|
||||
|
||||
return kolab_driver::to_rcube_event($record);
|
||||
}
|
||||
|
|
|
@ -772,7 +772,7 @@ class kolab_driver extends calendar_driver
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($success && $this->freebusy_trigger)
|
||||
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
|
||||
|
||||
|
@ -980,7 +980,7 @@ class kolab_driver extends calendar_driver
|
|||
$success = $storage->update_event($event);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if ($success && $this->freebusy_trigger)
|
||||
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
|
||||
|
||||
|
@ -1215,6 +1215,24 @@ class kolab_driver extends calendar_driver
|
|||
return $cal->storage->get_attachment($event['id'], $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a struct representing the given message reference
|
||||
*
|
||||
* @see calendar_driver::get_message_reference()
|
||||
*/
|
||||
public function get_message_reference($uri_or_headers, $folder = null)
|
||||
{
|
||||
if (is_object($uri_or_headers)) {
|
||||
$uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
|
||||
}
|
||||
|
||||
if (is_string($uri_or_headers)) {
|
||||
return kolab_storage_config::get_message_reference($uri_or_headers, 'event');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* List availabale categories
|
||||
* The default implementation reads them from config/user prefs
|
||||
|
@ -1403,7 +1421,6 @@ class kolab_driver extends calendar_driver
|
|||
return $record;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provide a list of revisions for the given event
|
||||
*
|
||||
|
|
|
@ -87,6 +87,7 @@ $labels['sensitivity'] = 'Privacy';
|
|||
$labels['public'] = 'public';
|
||||
$labels['private'] = 'private';
|
||||
$labels['confidential'] = 'confidential';
|
||||
$labels['links'] = 'Reference';
|
||||
$labels['alarms'] = 'Reminder';
|
||||
$labels['comment'] = 'Comment';
|
||||
$labels['created'] = 'Created';
|
||||
|
@ -95,6 +96,7 @@ $labels['unknown'] = 'Unknown';
|
|||
$labels['eventoptions'] = 'Options';
|
||||
$labels['generated'] = 'generated at';
|
||||
$labels['eventhistory'] = 'History';
|
||||
$labels['removelink'] = 'Remove email reference';
|
||||
$labels['printdescriptions'] = 'Print descriptions';
|
||||
$labels['parentcalendar'] = 'Insert inside';
|
||||
$labels['searchearlierdates'] = '« Search for earlier events';
|
||||
|
|
|
@ -958,6 +958,37 @@ div.form-section,
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#event-links .attachmentslist {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#event-links label,
|
||||
#edit-event-links label {
|
||||
float: left;
|
||||
margin-top: 0.3em;
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
|
||||
#edit-event-links .event-text {
|
||||
margin-left: 8em;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
#edit-event-links .attachmentslist li.message a.messagelink,
|
||||
#event-links .attachmentslist li.message a.messagelink {
|
||||
padding: 0 0 0 24px;
|
||||
}
|
||||
|
||||
#edit-event-links .attachmentslist li a.delete {
|
||||
top: 0;
|
||||
background-position: -6px -378px;
|
||||
}
|
||||
|
||||
#edit-event-links .attachmentslist li.deleted a.messagelink,
|
||||
#edit-event-links .attachmentslist li.deleted a.messagelink:hover {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
#eventedit .formtable td.label {
|
||||
min-width: 6em;
|
||||
}
|
||||
|
|
|
@ -135,6 +135,11 @@
|
|||
<label><roundcube:label name="calendar.sensitivity" /></label>
|
||||
<span class="event-text"></span>
|
||||
</div>
|
||||
<div class="event-section" id="event-links">
|
||||
<label><roundcube:label name="calendar.links" /></label>
|
||||
<span class="event-text"></span>
|
||||
<br style="clear:left">
|
||||
</div>
|
||||
<div class="event-section" id="event-attachments">
|
||||
<label><roundcube:label name="attachments" /></label>
|
||||
<div class="event-text"></div>
|
||||
|
|
|
@ -70,6 +70,11 @@
|
|||
<label for="edit-sensitivity"><roundcube:label name="calendar.sensitivity" /></label>
|
||||
<roundcube:object name="plugin.sensitivity_select" id="edit-sensitivity" />
|
||||
</div>
|
||||
<div class="event-section" id="edit-event-links">
|
||||
<label><roundcube:label name="calendar.links" /></label>
|
||||
<div class="event-text"></div>
|
||||
<br style="clear:left">
|
||||
</div>
|
||||
</div>
|
||||
<!-- recurrence settings -->
|
||||
<div id="event-panel-recurrence">
|
||||
|
|
|
@ -788,6 +788,39 @@ function rcube_libcalendaring(settings)
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
// Render message reference links to the given container
|
||||
this.render_message_links = function(links, container, edit, plugin)
|
||||
{
|
||||
var ul = $('<ul>').addClass('attachmentslist');
|
||||
|
||||
$.each(links, function(i, link) {
|
||||
if (!link.mailurl)
|
||||
return true; // continue
|
||||
|
||||
var li = $('<li>').addClass('link')
|
||||
.addClass('message eml')
|
||||
.append($('<a>')
|
||||
.attr('href', link.mailurl)
|
||||
.addClass('messagelink')
|
||||
.text(link.subject || link.uri)
|
||||
)
|
||||
.appendTo(ul);
|
||||
|
||||
// add icon to remove the link
|
||||
if (edit) {
|
||||
$('<a>')
|
||||
.attr('href', '#delete')
|
||||
.attr('title', rcmail.gettext('removelink', plugin))
|
||||
.attr('data-uri', link.uri)
|
||||
.addClass('delete')
|
||||
.text(rcmail.gettext('delete'))
|
||||
.appendTo(li);
|
||||
}
|
||||
});
|
||||
|
||||
container.empty().append(ul);
|
||||
}
|
||||
}
|
||||
|
||||
////// static methods
|
||||
|
|
|
@ -129,6 +129,7 @@ function rcube_tasklist_ui(settings)
|
|||
var parse_datetime = this.parse_datetime;
|
||||
var date2unixtime = this.date2unixtime;
|
||||
var fromunixtime = this.fromunixtime;
|
||||
var render_message_links = this.render_message_links;
|
||||
|
||||
/**
|
||||
* initialize the tasks UI
|
||||
|
@ -597,6 +598,12 @@ function rcube_tasklist_ui(settings)
|
|||
return false;
|
||||
});
|
||||
|
||||
// register click handler for message delete buttons
|
||||
$('#taskedit-links').on('click', 'li a.delete', function(e) {
|
||||
remove_link(e.target);
|
||||
return false;
|
||||
});
|
||||
|
||||
// handle global document clicks: close popup menus
|
||||
$(document.body).click(clear_popups);
|
||||
|
||||
|
@ -1840,7 +1847,7 @@ function rcube_tasklist_ui(settings)
|
|||
// build attachments list
|
||||
$('#task-links').hide();
|
||||
if ($.isArray(rec.links) && rec.links.length) {
|
||||
task_show_links(rec.links || [], $('#task-links').children('.task-text'));
|
||||
render_message_links(rec.links || [], $('#task-links').children('.task-text'), false, 'tasklist');
|
||||
$('#task-links').show();
|
||||
}
|
||||
|
||||
|
@ -2052,7 +2059,7 @@ function rcube_tasklist_ui(settings)
|
|||
me.set_alarms_edit('#taskedit-alarms', action != 'new' && rec.valarms ? rec.valarms : []);
|
||||
|
||||
if ($.isArray(rec.links) && rec.links.length) {
|
||||
task_show_links(rec.links, $('#taskedit-links .task-text'), true);
|
||||
render_message_links(rec.links, $('#taskedit-links .task-text'), true, 'tasklist');
|
||||
$('#taskedit-links').show();
|
||||
}
|
||||
else {
|
||||
|
@ -2358,48 +2365,15 @@ function rcube_tasklist_ui(settings)
|
|||
/**
|
||||
*
|
||||
*/
|
||||
function task_show_links(links, container, edit)
|
||||
function remove_link(elem)
|
||||
{
|
||||
var dellink, ul = $('<ul>').addClass('attachmentslist');
|
||||
var $elem = $(elem), uri = $elem.attr('data-uri');
|
||||
|
||||
$.each(links, function(i, link) {
|
||||
var li = $('<li>').addClass('link')
|
||||
.addClass('message eml')
|
||||
.append($('<a>')
|
||||
.attr('href', link.mailurl)
|
||||
.addClass('messagelink')
|
||||
.text(link.subject || link.uri)
|
||||
)
|
||||
.appendTo(ul);
|
||||
|
||||
// add icon to remove the link
|
||||
if (edit) {
|
||||
$('<a>')
|
||||
.attr('href', '#delete')
|
||||
.attr('title', rcmail.gettext('removelink','tasklist'))
|
||||
.addClass('delete')
|
||||
.text(rcmail.gettext('delete'))
|
||||
.click({ uri:link.uri }, function(e) {
|
||||
remove_link(this, e.data.uri);
|
||||
return false;
|
||||
})
|
||||
.appendTo(li);
|
||||
}
|
||||
});
|
||||
|
||||
container.empty().append(ul);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function remove_link(elem, uri)
|
||||
{
|
||||
// remove the link item matching the given uri
|
||||
me.selected_task.links = $.grep(me.selected_task.links, function(link) { return link.uri != uri; });
|
||||
|
||||
// remove UI list item
|
||||
$(elem).hide().closest('li').addClass('deleted');
|
||||
$elem.hide().closest('li').addClass('deleted');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue