mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03:00
Some cleanups for new AddressBook
This commit is contained in:
parent
c162a3acc6
commit
052f34a9f5
4 changed files with 43 additions and 72 deletions
|
|
@ -15,7 +15,6 @@ export class ContactModel extends AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.properties = [];
|
|
||||||
this.jCard = new JCard();
|
this.jCard = new JCard();
|
||||||
|
|
||||||
this.addObservables({
|
this.addObservables({
|
||||||
|
|
@ -30,8 +29,7 @@ export class ContactModel extends AbstractModel {
|
||||||
surName: '', // LastName
|
surName: '', // LastName
|
||||||
middleName: '', // MiddleName
|
middleName: '', // MiddleName
|
||||||
namePrefix: '', // NamePrefix
|
namePrefix: '', // NamePrefix
|
||||||
nameSuffix: '', // NameSuffix
|
nameSuffix: '' // NameSuffix
|
||||||
nickname: null
|
|
||||||
});
|
});
|
||||||
// this.email = koArrayWithDestroy();
|
// this.email = koArrayWithDestroy();
|
||||||
this.email = ko.observableArray();
|
this.email = ko.observableArray();
|
||||||
|
|
@ -43,6 +41,15 @@ export class ContactModel extends AbstractModel {
|
||||||
|
|
||||||
fullName: () => (this.givenName() + ' ' + this.surName()).trim(),
|
fullName: () => (this.givenName() + ' ' + this.surName()).trim(),
|
||||||
|
|
||||||
|
nickname: {
|
||||||
|
read: () => {
|
||||||
|
let prop = this.jCard.getOne('nickname');
|
||||||
|
return prop && prop.value;
|
||||||
|
},
|
||||||
|
write: value =>
|
||||||
|
value ? this.jCard.set('nickname', value/*, params, group*/) : this.jCard.remove('nickname')
|
||||||
|
},
|
||||||
|
|
||||||
display: () => {
|
display: () => {
|
||||||
let a = this.jCard.getOne('fn')?.value,
|
let a = this.jCard.getOne('fn')?.value,
|
||||||
b = this.fullName(),
|
b = this.fullName(),
|
||||||
|
|
@ -85,17 +92,12 @@ export class ContactModel extends AbstractModel {
|
||||||
static reviveFromJson(json) {
|
static reviveFromJson(json) {
|
||||||
const contact = super.reviveFromJson(json);
|
const contact = super.reviveFromJson(json);
|
||||||
if (contact) {
|
if (contact) {
|
||||||
let list = [];
|
|
||||||
|
|
||||||
let jCard = new JCard(json.jCard),
|
let jCard = new JCard(json.jCard),
|
||||||
props = jCard.getOne('n')?.value;
|
props = jCard.getOne('n')?.value;
|
||||||
props && props.forEach((value, index) =>
|
props && props.forEach((value, index) =>
|
||||||
value && contact[nProps[index]](value)
|
value && contact[nProps[index]](value)
|
||||||
);
|
);
|
||||||
|
|
||||||
props = jCard.getOne('nickname');
|
|
||||||
props && contact.nickname(props.value);
|
|
||||||
|
|
||||||
['email', 'tel', 'url'].forEach(field => {
|
['email', 'tel', 'url'].forEach(field => {
|
||||||
props = jCard.get(field);
|
props = jCard.get(field);
|
||||||
props && props.forEach(prop => {
|
props && props.forEach(prop => {
|
||||||
|
|
@ -106,7 +108,6 @@ export class ContactModel extends AbstractModel {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
contact.properties = list;
|
|
||||||
contact.jCard = jCard;
|
contact.jCard = jCard;
|
||||||
}
|
}
|
||||||
return contact;
|
return contact;
|
||||||
|
|
@ -160,8 +161,6 @@ export class ContactModel extends AbstractModel {
|
||||||
]/*, params, group*/);
|
]/*, params, group*/);
|
||||||
// jCard.parseFullName({set:true});
|
// jCard.parseFullName({set:true});
|
||||||
|
|
||||||
this.nickname() ? jCard.set('nickname', this.nickname()/*, params, group*/) : jCard.remove('nickname');
|
|
||||||
|
|
||||||
['email', 'tel', 'url'].forEach(field => {
|
['email', 'tel', 'url'].forEach(field => {
|
||||||
let values = this[field].map(item => item.value());
|
let values = this[field].map(item => item.value());
|
||||||
jCard.get(field).forEach(prop => {
|
jCard.get(field).forEach(prop => {
|
||||||
|
|
|
||||||
|
|
@ -148,10 +148,6 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.read-only .button-save-contact {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.b-view-content {
|
.b-view-content {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||||
import { AskPopupView } from 'View/Popup/Ask';
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
const
|
const
|
||||||
viewProperties = koArrayWithDestroy(),
|
|
||||||
CONTACTS_PER_PAGE = 50,
|
CONTACTS_PER_PAGE = 50,
|
||||||
ScopeContacts = 'Contacts';
|
ScopeContacts = 'Contacts';
|
||||||
|
|
||||||
|
|
@ -38,20 +37,15 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
search: '',
|
search: '',
|
||||||
contactsCount: 0,
|
contactsCount: 0,
|
||||||
|
|
||||||
currentContact: null,
|
selectorContact: null,
|
||||||
|
|
||||||
importUploaderButton: null,
|
importButton: null,
|
||||||
|
|
||||||
contactsPage: 1,
|
contactsPage: 1,
|
||||||
|
|
||||||
viewClearSearch: false,
|
|
||||||
|
|
||||||
viewReadOnly: false,
|
|
||||||
|
|
||||||
viewSaving: false,
|
viewSaving: false,
|
||||||
|
|
||||||
watchDirty: false,
|
hasChanges: false,
|
||||||
watchHash: false,
|
|
||||||
|
|
||||||
contact: null
|
contact: null
|
||||||
});
|
});
|
||||||
|
|
@ -62,7 +56,7 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
this.selector = new Selector(
|
this.selector = new Selector(
|
||||||
ContactUserStore,
|
ContactUserStore,
|
||||||
this.currentContact,
|
this.selectorContact,
|
||||||
null,
|
null,
|
||||||
'.e-contact-item .actionHandle',
|
'.e-contact-item .actionHandle',
|
||||||
'.e-contact-item .checkboxItem',
|
'.e-contact-item .checkboxItem',
|
||||||
|
|
@ -87,7 +81,7 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
contactsCheckedOrSelected: () => {
|
contactsCheckedOrSelected: () => {
|
||||||
const checked = ContactUserStore.filter(item => item.checked && item.checked()),
|
const checked = ContactUserStore.filter(item => item.checked && item.checked()),
|
||||||
selected = this.currentContact();
|
selected = this.selectorContact();
|
||||||
|
|
||||||
return selected
|
return selected
|
||||||
? [...checked, selected].unique()
|
? [...checked, selected].unique()
|
||||||
|
|
@ -96,34 +90,27 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
contactsCheckedOrSelectedUids: () => this.contactsCheckedOrSelected().map(contact => contact.id()),
|
contactsCheckedOrSelectedUids: () => this.contactsCheckedOrSelected().map(contact => contact.id()),
|
||||||
|
|
||||||
contactsSyncEnabled: () => ContactUserStore.allowSync() && ContactUserStore.syncMode(),
|
contactsSyncEnabled: () => ContactUserStore.allowSync() && ContactUserStore.syncMode()
|
||||||
|
|
||||||
viewHash: () => '' + viewProperties.map(property => property.value && property.value()).join('')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.search.subscribe(() => this.reloadContactList());
|
this.search.subscribe(() => this.reloadContactList());
|
||||||
|
|
||||||
this.viewHash.subscribe(() => {
|
|
||||||
if (this.watchHash() && !this.viewReadOnly() && !this.watchDirty()) {
|
|
||||||
this.watchDirty(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.saveCommand = this.saveCommand.bind(this);
|
this.saveCommand = this.saveCommand.bind(this);
|
||||||
|
|
||||||
|
// this.hasChanges(!!contact()?.toJSON().jCard);
|
||||||
|
|
||||||
decorateKoCommands(this, {
|
decorateKoCommands(this, {
|
||||||
// close: self => !self.watchDirty(),
|
// close: self => !self.hasChanges(),
|
||||||
deleteCommand: self => 0 < self.contactsCheckedOrSelected().length,
|
deleteCommand: self => 0 < self.contactsCheckedOrSelected().length,
|
||||||
newMessageCommand: self => 0 < self.contactsCheckedOrSelected().length,
|
newMessageCommand: self => 0 < self.contactsCheckedOrSelected().length,
|
||||||
saveCommand: self => !self.viewSaving() && !self.viewReadOnly()
|
saveCommand: self => !self.viewSaving() && !self.hasChanges(),
|
||||||
&& (self.contact()?.hasValidName() || self.contact()?.email().length),
|
|
||||||
syncCommand: self => !self.contacts.syncing() && !self.contacts.importing()
|
syncCommand: self => !self.contacts.syncing() && !self.contacts.importing()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
newContact() {
|
newContact() {
|
||||||
this.populateViewContact(null);
|
this.populateViewContact(null);
|
||||||
this.currentContact(null);
|
this.selectorContact(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCommand() {
|
deleteCommand() {
|
||||||
|
|
@ -207,7 +194,7 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
this.watchDirty(false);
|
this.hasChanges(false);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
RequestUid: requestUid,
|
RequestUid: requestUid,
|
||||||
|
|
@ -237,14 +224,14 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
removeCheckedOrSelectedContactsFromList() {
|
removeCheckedOrSelectedContactsFromList() {
|
||||||
const contacts = this.contactsCheckedOrSelected();
|
const contacts = this.contactsCheckedOrSelected();
|
||||||
|
|
||||||
let currentContact = this.currentContact(),
|
let selectorContact = this.selectorContact(),
|
||||||
count = ContactUserStore.length;
|
count = ContactUserStore.length;
|
||||||
|
|
||||||
if (contacts.length) {
|
if (contacts.length) {
|
||||||
contacts.forEach(contact => {
|
contacts.forEach(contact => {
|
||||||
if (currentContact && currentContact.id() === contact.id()) {
|
if (selectorContact && selectorContact.id() === contact.id()) {
|
||||||
currentContact = null;
|
selectorContact = null;
|
||||||
this.currentContact(null);
|
this.selectorContact(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
contact.deleted(true);
|
contact.deleted(true);
|
||||||
|
|
@ -282,18 +269,12 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
* @param {?ContactModel} contact
|
* @param {?ContactModel} contact
|
||||||
*/
|
*/
|
||||||
populateViewContact(contact) {
|
populateViewContact(contact) {
|
||||||
this.watchHash(false);
|
|
||||||
|
|
||||||
if (!contact) {
|
if (!contact) {
|
||||||
contact = new ContactModel;
|
contact = new ContactModel;
|
||||||
}
|
}
|
||||||
this.viewReadOnly(contact.readOnly());
|
|
||||||
this.contact(contact);
|
this.contact(contact);
|
||||||
|
|
||||||
viewProperties(contact.properties);
|
this.hasChanges(false);
|
||||||
|
|
||||||
this.watchDirty(false);
|
|
||||||
this.watchHash(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -330,7 +311,6 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
ContactUserStore(list);
|
ContactUserStore(list);
|
||||||
|
|
||||||
ContactUserStore.loading(false);
|
ContactUserStore.loading(false);
|
||||||
this.viewClearSearch(!!this.search());
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Offset: offset,
|
Offset: offset,
|
||||||
|
|
@ -368,12 +348,12 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
// initUploader
|
// initUploader
|
||||||
|
|
||||||
if (this.importUploaderButton()) {
|
if (this.importButton()) {
|
||||||
const j = new Jua({
|
const j = new Jua({
|
||||||
action: serverRequest('UploadContacts'),
|
action: serverRequest('UploadContacts'),
|
||||||
limit: 1,
|
limit: 1,
|
||||||
disableDocumentDropPrevent: true,
|
disableDocumentDropPrevent: true,
|
||||||
clickElement: this.importUploaderButton()
|
clickElement: this.importButton()
|
||||||
});
|
});
|
||||||
|
|
||||||
if (j) {
|
if (j) {
|
||||||
|
|
@ -391,7 +371,7 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
if (this.watchDirty() && AskPopupView.hidden()) {
|
if (this.hasChanges() && AskPopupView.hidden()) {
|
||||||
showScreenPopup(AskPopupView, [
|
showScreenPopup(AskPopupView, [
|
||||||
i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
|
i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
|
||||||
() => this.close()
|
() => this.close()
|
||||||
|
|
@ -408,17 +388,15 @@ export class ContactsPopupView extends AbstractViewPopup {
|
||||||
|
|
||||||
onHide() {
|
onHide() {
|
||||||
this.contact(null);
|
this.contact(null);
|
||||||
this.currentContact(null);
|
this.selectorContact(null);
|
||||||
this.search('');
|
this.search('');
|
||||||
this.contactsCount(0);
|
this.contactsCount(0);
|
||||||
|
|
||||||
ContactUserStore([]);
|
ContactUserStore([]);
|
||||||
|
|
||||||
this.sLastComposeFocusedField = '';
|
this.sLastComposeFocusedField = '';
|
||||||
|
|
||||||
if (this.bBackToCompose) {
|
if (this.bBackToCompose) {
|
||||||
this.bBackToCompose = false;
|
this.bBackToCompose = false;
|
||||||
|
|
||||||
showMessageComposer();
|
showMessageComposer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<a id="contacts-more-dropdown-id" class="btn dropdown-toggle fontastic" href="#" tabindex="-1">☰</a>
|
<a id="contacts-more-dropdown-id" class="btn dropdown-toggle fontastic" href="#" tabindex="-1">☰</a>
|
||||||
<menu class="dropdown-menu right-edge" role="menu" aria-labelledby="contacts-more-dropdown-id">
|
<menu class="dropdown-menu right-edge" role="menu" aria-labelledby="contacts-more-dropdown-id">
|
||||||
<li role="presentation">
|
<li role="presentation">
|
||||||
<a href="#" tabindex="-1" data-bind="initDom: importUploaderButton">
|
<a href="#" tabindex="-1" data-bind="initDom: importButton">
|
||||||
<i data-bind="css: {'icon-import': !contacts.importing(), 'icon-spinner': contacts.importing}"></i>
|
<i data-bind="css: {'icon-import': !contacts.importing(), 'icon-spinner': contacts.importing}"></i>
|
||||||
<span data-i18n="CONTACTS/BUTTON_IMPORT"></span>
|
<span data-i18n="CONTACTS/BUTTON_IMPORT"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="b-list-content g-ui-user-select-none" data-bind="css: {'hideContactListCheckbox': !useCheckboxesInList()}">
|
<div class="b-list-content g-ui-user-select-none" data-bind="css: {'hideContactListCheckbox': !useCheckboxesInList()}">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="listClear" data-bind="visible: viewClearSearch() && '' !== search()">
|
<div class="listClear" data-bind="visible: !!search()">
|
||||||
<span class="g-ui-link" data-i18n="CONTACTS/CLEAR_SEARCH" data-bind="click: clearSearch"></span>
|
<span class="g-ui-link" data-i18n="CONTACTS/CLEAR_SEARCH" data-bind="click: clearSearch"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="listEmptyList" data-bind="visible: 0 === contacts().length && '' === search() && !contacts.loading()"
|
<div class="listEmptyList" data-bind="visible: 0 === contacts().length && '' === search() && !contacts.loading()"
|
||||||
|
|
@ -72,16 +72,16 @@
|
||||||
<!-- ko template: { name: 'Paginator', data: contactsPaginator } --><!-- /ko -->
|
<!-- ko template: { name: 'Paginator', data: contactsPaginator } --><!-- /ko -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="b-view-content-toolbar btn-toolbar" data-bind="css: {'read-only': viewReadOnly}, i18nUpdate: contact">
|
<div class="b-view-content-toolbar btn-toolbar" data-bind="i18nUpdate: contact">
|
||||||
<!-- ko with: contact -->
|
<!-- ko with: contact -->
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn button-save-contact" data-bind="command: $root.saveCommand, css: {'dirty': $root.watchDirty}">
|
<button class="btn button-save-contact" data-bind="visible: !readOnly(), command: $root.saveCommand, css: {'dirty': $root.hasChanges}">
|
||||||
<i data-bind="css: {'icon-ok': !$root.viewSaving(), 'icon-spinner': $root.viewSaving()}"></i>
|
<i data-bind="css: {'icon-ok': !$root.viewSaving(), 'icon-spinner': $root.viewSaving()}"></i>
|
||||||
<span data-i18n="CONTACTS/BUTTON_CREATE_CONTACT" data-bind="visible: !id"></span>
|
<span data-i18n="CONTACTS/BUTTON_CREATE_CONTACT" data-bind="visible: !id()"></span>
|
||||||
<span data-i18n="CONTACTS/BUTTON_UPDATE_CONTACT" data-bind="visible: id"></span>
|
<span data-i18n="CONTACTS/BUTTON_UPDATE_CONTACT" data-bind="visible: id"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true">
|
<div class="btn-group dropdown" data-bind="visible: !readOnly(), registerBootstrapDropdown: true">
|
||||||
<a id="button-add-prop-dropdown-id" href="#" tabindex="-1" class="btn dropdown-toggle" data-i18n="CONTACTS/ADD_MENU_LABEL"></a>
|
<a id="button-add-prop-dropdown-id" href="#" tabindex="-1" class="btn dropdown-toggle" data-i18n="CONTACTS/ADD_MENU_LABEL"></a>
|
||||||
<menu class="dropdown-menu right-edge" style="text-align: left" tabindex="-1" role="menu" aria-labelledby="button-add-prop-dropdown-id">
|
<menu class="dropdown-menu right-edge" style="text-align: left" tabindex="-1" role="menu" aria-labelledby="button-add-prop-dropdown-id">
|
||||||
<li role="presentation">
|
<li role="presentation">
|
||||||
|
|
@ -118,15 +118,15 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
</div>
|
</div>
|
||||||
<div class="b-view-content" data-bind="css: {'read-only': viewReadOnly}">
|
<div class="b-view-content">
|
||||||
<div class="b-contact-view-desc" data-bind="visible: !contact"
|
<div class="b-contact-view-desc" data-bind="visible: !contact"
|
||||||
data-i18n="CONTACTS/CONTACT_VIEW_DESC"></div>
|
data-i18n="CONTACTS/CONTACT_VIEW_DESC"></div>
|
||||||
<div data-bind="visible: contact, i18nUpdate: contact">
|
<div data-bind="visible: contact, i18nUpdate: contact">
|
||||||
<div class="form-horizontal top-part">
|
<!-- ko with: contact -->
|
||||||
<div class="control-group" data-bind="visible: !viewReadOnly() || hasValidName()">
|
<div class="form-horizontal top-part" data-bind="css: {'read-only': readOnly}">
|
||||||
|
<div class="control-group" data-bind="visible: !readOnly() || hasValidName()">
|
||||||
<label class="fontastic iconsize24">👤</label>
|
<label class="fontastic iconsize24">👤</label>
|
||||||
<div>
|
<div>
|
||||||
<!-- ko with: contact -->
|
|
||||||
<div class="property-line">
|
<div class="property-line">
|
||||||
<span data-bind="text: givenName"></span>
|
<span data-bind="text: givenName"></span>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
|
|
@ -145,10 +145,8 @@
|
||||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||||
data-bind="value: nickname" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_NICK_NAME">
|
data-bind="value: nickname" data-i18n="[placeholder]CONTACTS/PLACEHOLDER_ENTER_NICK_NAME">
|
||||||
</div>
|
</div>
|
||||||
<!-- /ko -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- ko with: contact -->
|
|
||||||
<div class="control-group" data-bind="email().length">
|
<div class="control-group" data-bind="email().length">
|
||||||
<label class="fontastic iconsize24" data-i18n="[title]GLOBAL/EMAIL">@</label>
|
<label class="fontastic iconsize24" data-i18n="[title]GLOBAL/EMAIL">@</label>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -191,8 +189,8 @@
|
||||||
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: $root.addUrl">✚</a>
|
<a href="#" class="btn fontastic" data-bind="visible: !readOnly(), click: $root.addUrl">✚</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /ko -->
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- /ko -->
|
||||||
<!--
|
<!--
|
||||||
<div class="e-read-only-sign fontastic iconsize24" data-i18n="[title]CONTACTS/LABEL_READ_ONLY">🔒</div>
|
<div class="e-read-only-sign fontastic iconsize24" data-i18n="[title]CONTACTS/LABEL_READ_ONLY">🔒</div>
|
||||||
-->
|
-->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue