mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-31 04:59:21 +03:00
Redesign AddressBook
This commit is contained in:
parent
5f6ea3cf29
commit
a71874a8c4
18 changed files with 1403 additions and 1669 deletions
227
dev/DAV/JCard.js
Normal file
227
dev/DAV/JCard.js
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
/**
|
||||
* https://datatracker.ietf.org/doc/html/rfc7095
|
||||
*
|
||||
* Inspired by https://github.com/mcpar-land/vcfer
|
||||
*/
|
||||
|
||||
import { VCardProperty } from './VCardProperty'
|
||||
|
||||
export class JCard {
|
||||
|
||||
constructor(input)
|
||||
{
|
||||
this.props = new Map()
|
||||
this.version = '4.0'
|
||||
if (input) {
|
||||
// read from jCard
|
||||
if (typeof input !== 'object') {
|
||||
throw new Error('error reading vcard')
|
||||
}
|
||||
this.parseFromJCard(input)
|
||||
}
|
||||
}
|
||||
|
||||
parseFromJCard(json)
|
||||
{
|
||||
json = JSON.parse(JSON.stringify(json));
|
||||
if (!/vcard/i.test(json[0])) {
|
||||
throw new SyntaxError('Incorrect jCard format');
|
||||
}
|
||||
json[1].forEach(jprop => this.add(new VCardProperty(jprop)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an array of {@link VCardProperty} objects under the specified field.
|
||||
* Returns [] if there are no VCardProperty objects found.
|
||||
* Properites are always stored in an array.
|
||||
* @param field to get.
|
||||
* @param type If provided, only return {@link VCardProperty}s with the specified
|
||||
* type as a param.
|
||||
*/
|
||||
get(field, type)
|
||||
{
|
||||
if (type) {
|
||||
let props = this.props.get(field);
|
||||
return props
|
||||
? props.filter(prop => {
|
||||
let types = prop.type;
|
||||
return (Array.isArray(types) ? types : [types]).includes(type);
|
||||
})
|
||||
: [];
|
||||
}
|
||||
return this.props.get(field) || [];
|
||||
// TODO with type filter-er
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a _single_ VCardProperty of the specified field. Attempts to pick based
|
||||
* on the following priorities, in order:
|
||||
* - `TYPE={type}` of the value specified in the `type` argument. Ignored
|
||||
* if the argument isn't supplied.
|
||||
* - `TYPE=pref` is present.
|
||||
* - is the VCardProperty at index 0 from get(field)
|
||||
* @param field
|
||||
* @param type
|
||||
*/
|
||||
getOne(field, type)
|
||||
{
|
||||
return this.get(field, type || 'pref')[0] || this.get(field)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of a field to contain a single {@link VCardProperty}.
|
||||
*
|
||||
* Accepts either 2-4 arguments to construct a VCardProperty,
|
||||
* or 1 argument of a preexisting VCardProperty object.
|
||||
*
|
||||
* This will always overwrite all existing properties of the given
|
||||
* field. For just adding a new VCardProperty, see {@link VCard#add}
|
||||
* @param arg the field, or a VCardProperty object
|
||||
* @param value the value for the VCardProperty object
|
||||
* @param params the parameters for the VCardProperty object
|
||||
* @param group the group for the VCardProperty object
|
||||
*/
|
||||
set(arg, value, params, group)
|
||||
{
|
||||
if (typeof arg === 'string') {
|
||||
arg = new VCardProperty(String(arg), value, params, group);
|
||||
}
|
||||
if (!(arg instanceof VCardProperty)) {
|
||||
throw new Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
||||
}
|
||||
let field = arg.getField();
|
||||
this.props.set(field, [arg]);
|
||||
return arg;
|
||||
}
|
||||
|
||||
add(arg, value, params, group)
|
||||
{
|
||||
// string arguments
|
||||
if (typeof arg === 'string') {
|
||||
arg = new VCardProperty(String(arg), value, params, group);
|
||||
}
|
||||
if (!(arg instanceof VCardProperty)) {
|
||||
throw new Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
||||
}
|
||||
// VCardProperty arguments
|
||||
let field = arg.getField();
|
||||
if (this.props.get(field)) this.props.get(field)?.push(arg)
|
||||
else this.props.set(field, [arg])
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link VCardProperty}, or all properties of the supplied field.
|
||||
* @param arg the field, or a {@link VCardProperty} object
|
||||
* @param paramFilter (incomplete)
|
||||
*/
|
||||
remove(arg) {
|
||||
// string arguments
|
||||
if (typeof arg === 'string') {
|
||||
// TODO filter by param
|
||||
this.props.delete(arg);
|
||||
}
|
||||
// VCardProperty argument
|
||||
else if (arg instanceof VCardProperty) {
|
||||
let propArray = this.props.get(arg.getField());
|
||||
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
|
||||
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||
propArray.splice(propArray.indexOf(arg), 1);
|
||||
if (propArray.length === 0)
|
||||
this.props.delete(arg.getField());
|
||||
}
|
||||
// incorrect arguments
|
||||
else
|
||||
throw new Error('invalid argument of VCard.remove(), expects ' +
|
||||
'string and optional param filter or a VCardProperty');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the vCard has at least one @{link VCardProperty}
|
||||
* of the given field.
|
||||
* @param field The field to query
|
||||
*/
|
||||
has(field)
|
||||
{
|
||||
return (!!this.props.get(field) && this.props.get(field).length > 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns stringified JSON
|
||||
*/
|
||||
toString()
|
||||
{
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link JCard} object as a JSON array.
|
||||
*/
|
||||
toJSON()
|
||||
{
|
||||
let data = [['version', {}, 'text', '4.0']];
|
||||
/*
|
||||
this.props.forEach((props, field) =>
|
||||
(field === 'version') || props.forEach(prop => prop.isEmpty() || data.push(prop.toJSON()))
|
||||
);
|
||||
*/
|
||||
for (const [field, props] of this.props.entries()) {
|
||||
if ('version' !== field) {
|
||||
for (const prop of props) {
|
||||
prop.isEmpty() || data.push(prop.toJSON());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['vcard', data];
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically generate the 'fn' VCardProperty from the preferred 'n' VCardProperty.
|
||||
*
|
||||
* #### `set` (`boolean`)
|
||||
*
|
||||
* - `false`: (default) return the generated full name string without
|
||||
* modifying the VCard.
|
||||
*
|
||||
* - `true`: modify the VCard's `fn` VCardProperty directly, as specified
|
||||
* by `append`
|
||||
*
|
||||
* #### `append` (`boolean`)
|
||||
*
|
||||
* (ignored if `set` is `false`)
|
||||
*
|
||||
* - `false`: (default) replace the existing 'fn' VCardProperty/properties with
|
||||
* a new one.
|
||||
*
|
||||
* - `true`: append a new `fn` VCardProperty to the array.
|
||||
*
|
||||
* see: [RFC 6350 section 6.2.1](https://tools.ietf.org/html/rfc6350#section-6.2.1)
|
||||
*/
|
||||
parseFullName(options) {
|
||||
let n = this.getOne('n');
|
||||
if (n === undefined) {
|
||||
throw new Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
||||
}
|
||||
let fnString = '';
|
||||
// Position in n -> position in fn
|
||||
[3, 1, 2, 0, 4].forEach(pos => {
|
||||
let splitStr = n.value[pos];
|
||||
if (splitStr) {
|
||||
// comma separated values separated by spaces
|
||||
fnString += ' ' + splitStr.replace(',', ' ');
|
||||
}
|
||||
});
|
||||
fnString = fnString.trim();
|
||||
let fn = new VCardProperty('fn', fnString);
|
||||
if (options?.set) {
|
||||
if (options.append) {
|
||||
this.add(fn);
|
||||
} else {
|
||||
this.set(fn);
|
||||
}
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
}
|
||||
152
dev/DAV/VCardProperty.js
Normal file
152
dev/DAV/VCardProperty.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* Inspired by https://github.com/mcpar-land/vcfer
|
||||
*/
|
||||
|
||||
const
|
||||
camelCase = str =>
|
||||
str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
|
||||
index === 0 ? word.toLowerCase() : word.toUpperCase()
|
||||
).replace(/\s+/g, '');
|
||||
|
||||
export class VCardProperty {
|
||||
|
||||
/**
|
||||
* A class describing a single vCard property.
|
||||
* Will almost always be a member of a
|
||||
* {@link VCard}'s [props]{@link VCard.props} map.
|
||||
*
|
||||
* Accepts either 2-4 arguments, or 1 argument in jCard property format.
|
||||
* @param arg the field, or a jCard property
|
||||
* @param value
|
||||
* @param params
|
||||
* @param type
|
||||
*/
|
||||
constructor(arg, value, params, type = 'text')
|
||||
{
|
||||
this.field = '';
|
||||
|
||||
/**
|
||||
* the value of the property.
|
||||
* @example '(123) 456 7890'
|
||||
*/
|
||||
this.value = '';
|
||||
|
||||
/**
|
||||
* the type of the property value.
|
||||
* @example 'text'
|
||||
*/
|
||||
this.type = '';
|
||||
|
||||
/**
|
||||
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5
|
||||
* An jCard parameters object.
|
||||
* @example
|
||||
* {
|
||||
* type: ['work', 'voice', 'pref'],
|
||||
* value: 'uri'
|
||||
* }
|
||||
*/
|
||||
this.params = {};
|
||||
|
||||
// Construct from arguments
|
||||
if (value !== undefined && typeof arg === 'string') {
|
||||
this.field = arg;
|
||||
this.value = value;
|
||||
this.params = params || {};
|
||||
this.type = type;
|
||||
}
|
||||
// construct from jcard
|
||||
else if (value === undefined && params === undefined && typeof arg === 'object') {
|
||||
this.parseFromJCardProperty(arg);
|
||||
}
|
||||
// invalid property
|
||||
else {
|
||||
throw new Error('invalid Property constructor');
|
||||
}
|
||||
}
|
||||
|
||||
parseFromJCardProperty(jCardProp)
|
||||
{
|
||||
jCardProp = JSON.parse(JSON.stringify(jCardProp));
|
||||
this.field = camelCase(jCardProp[0]);
|
||||
this.params = jCardProp[1];
|
||||
this.type = jCardProp[2];
|
||||
this.value = jCardProp[3];
|
||||
}
|
||||
|
||||
addParam(key, value)
|
||||
{
|
||||
if (Array.isArray(this.params[key])) {
|
||||
this.params[key].push(value);
|
||||
}
|
||||
else if (this.params[key] != null) {
|
||||
this.params[key] = [this.params[key], value];
|
||||
}
|
||||
else {
|
||||
this.params[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a copy of the Property's string value. */
|
||||
getValue()
|
||||
{
|
||||
return '' + this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.3
|
||||
*/
|
||||
pref()
|
||||
{
|
||||
return this.params.pref || 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.6
|
||||
*/
|
||||
tags()
|
||||
{
|
||||
return this.params.type || [];
|
||||
}
|
||||
|
||||
/** Returns `true` if all the following are true:
|
||||
* - the property's value contains charactes other than `;`
|
||||
* - the property has no parameters
|
||||
*/
|
||||
isEmpty()
|
||||
{
|
||||
return ((null == this.value || !/[^;]+/.test(this.value)) && !Object.keys(this.params).length);
|
||||
}
|
||||
|
||||
notEmpty()
|
||||
{
|
||||
return !this.isEmpty();
|
||||
}
|
||||
|
||||
/** Returns a readonly string copy of the property's field. */
|
||||
getField()
|
||||
{
|
||||
return '' + this.field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns stringified JSON
|
||||
*/
|
||||
toString()
|
||||
{
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON array in a [jCard property]{@link JCardProperty} format
|
||||
*/
|
||||
toJSON()
|
||||
{
|
||||
return [
|
||||
this.field,
|
||||
this.params,
|
||||
this.type || 'text',
|
||||
this.value
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,63 @@
|
|||
import { arrayLength } from 'Common/Utils';
|
||||
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
|
||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||
|
||||
import { JCard } from 'DAV/JCard';
|
||||
//import { VCardProperty } from 'DAV/VCardProperty';
|
||||
|
||||
const nProps = [
|
||||
'surName',
|
||||
'givenName',
|
||||
'middleName',
|
||||
'namePrefix',
|
||||
'nameSuffix'
|
||||
];
|
||||
|
||||
export class ContactModel extends AbstractModel {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.id = 0;
|
||||
this.display = '';
|
||||
this.properties = [];
|
||||
this.readOnly = false;
|
||||
this.jCard = new JCard();
|
||||
|
||||
this.addObservables({
|
||||
focused: false,
|
||||
selected: false,
|
||||
checked: false,
|
||||
deleted: false
|
||||
deleted: false,
|
||||
readOnly: false,
|
||||
|
||||
id: 0,
|
||||
givenName: '', // FirstName
|
||||
surName: '', // LastName
|
||||
middleName: '', // MiddleName
|
||||
namePrefix: '', // NamePrefix
|
||||
nameSuffix: '', // NameSuffix
|
||||
nickname: null
|
||||
});
|
||||
// this.email = koArrayWithDestroy();
|
||||
this.email = ko.observableArray();
|
||||
this.tel = ko.observableArray();
|
||||
this.url = ko.observableArray();
|
||||
|
||||
this.addComputables({
|
||||
hasValidName: () => !!(this.givenName() || this.surName()),
|
||||
|
||||
fullName: () => (this.givenName() + ' ' + this.surName()).trim(),
|
||||
|
||||
display: () => {
|
||||
let a = this.jCard.getOne('fn')?.value,
|
||||
b = this.fullName(),
|
||||
c = this.jCard.getOne('email')?.value,
|
||||
d = this.nickname();
|
||||
return a || b || c || d;
|
||||
}
|
||||
/*
|
||||
fullName: {
|
||||
read: () => this.givenName() + " " + this.surName(),
|
||||
write: value => {
|
||||
this.jCard.set('fn', value/*, params, group* /)
|
||||
}
|
||||
}
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -23,23 +65,15 @@ export class ContactModel extends AbstractModel {
|
|||
* @returns {Array|null}
|
||||
*/
|
||||
getNameAndEmailHelper() {
|
||||
let name = '',
|
||||
email = '';
|
||||
|
||||
if (arrayLength(this.properties)) {
|
||||
this.properties.forEach(property => {
|
||||
if (property) {
|
||||
if (ContactPropertyType.FirstName === property.type()) {
|
||||
name = (property.value() + ' ' + name).trim();
|
||||
} else if (ContactPropertyType.LastName === property.type()) {
|
||||
name = (name + ' ' + property.value()).trim();
|
||||
} else if (!email && ContactPropertyType.Email === property.type()) {
|
||||
email = property.value();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let name = (this.givenName() + ' ' + this.surName()).trim(),
|
||||
email = this.email()[0];
|
||||
/*
|
||||
// this.jCard.getOne('fn')?.notEmpty() ||
|
||||
this.jCard.parseFullName({set:true});
|
||||
// let name = this.jCard.getOne('nickname'),
|
||||
let name = this.jCard.getOne('fn'),
|
||||
email = this.jCard.getOne('email');
|
||||
*/
|
||||
return email ? [email, name] : null;
|
||||
}
|
||||
|
||||
|
|
@ -52,47 +86,32 @@ export class ContactModel extends AbstractModel {
|
|||
const contact = super.reviveFromJson(json);
|
||||
if (contact) {
|
||||
let list = [];
|
||||
if (arrayLength(json.properties)) {
|
||||
json.properties.forEach(property => {
|
||||
property = ContactPropertyModel.reviveFromJson(property);
|
||||
property && list.push(property);
|
||||
|
||||
let jCard = new JCard(json.jCard),
|
||||
props = jCard.getOne('n')?.value;
|
||||
props && props.forEach((value, index) =>
|
||||
value && contact[nProps[index]](value)
|
||||
);
|
||||
|
||||
props = jCard.getOne('nickname');
|
||||
props && contact.nickname(props.value);
|
||||
|
||||
['email', 'tel', 'url'].forEach(field => {
|
||||
props = jCard.get(field);
|
||||
props && props.forEach(prop => {
|
||||
contact[field].push({
|
||||
value: ko.observable(prop.value)
|
||||
// type: prop.params.type
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
contact.properties = list;
|
||||
contact.initDefaultProperties();
|
||||
contact.jCard = jCard;
|
||||
}
|
||||
return contact;
|
||||
}
|
||||
|
||||
initDefaultProperties() {
|
||||
let list = this.properties;
|
||||
list.sort((p1,p2) =>{
|
||||
if (p2.type() == ContactPropertyType.FirstName) {
|
||||
return 1;
|
||||
}
|
||||
if (p1.type() == ContactPropertyType.FirstName || p1.type() == ContactPropertyType.LastName) {
|
||||
return -1;
|
||||
}
|
||||
if (p2.type() == ContactPropertyType.LastName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
let found = list.find(prop => prop.type() == ContactPropertyType.LastName);
|
||||
if (!found) {
|
||||
found = new ContactPropertyModel(ContactPropertyType.LastName);
|
||||
list.unshift(found);
|
||||
}
|
||||
found.placeholder('CONTACTS/PLACEHOLDER_ENTER_LAST_NAME');
|
||||
found = list.find(prop => prop.type() == ContactPropertyType.FirstName);
|
||||
if (!found) {
|
||||
found = new ContactPropertyModel(ContactPropertyType.FirstName);
|
||||
list.unshift(found);
|
||||
}
|
||||
found.placeholder('CONTACTS/PLACEHOLDER_ENTER_FIRST_NAME');
|
||||
this.properties = list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
|
|
@ -100,6 +119,71 @@ export class ContactModel extends AbstractModel {
|
|||
return ''+this.id;
|
||||
}
|
||||
|
||||
addEmail() {
|
||||
// home, work
|
||||
this.email.push({
|
||||
value: ko.observable('')
|
||||
// type: prop.params.type
|
||||
});
|
||||
}
|
||||
|
||||
addTel() {
|
||||
// home, work, text, voice, fax, cell, video, pager, textphone, iana-token, x-name
|
||||
this.tel.push({
|
||||
value: ko.observable('')
|
||||
// type: prop.params.type
|
||||
});
|
||||
}
|
||||
|
||||
addUrl() {
|
||||
// home, work
|
||||
this.url.push({
|
||||
value: ko.observable('')
|
||||
// type: prop.params.type
|
||||
});
|
||||
}
|
||||
|
||||
addNickname() {
|
||||
// home, work
|
||||
this.nickname() || this.nickname('');
|
||||
}
|
||||
|
||||
toJSON()
|
||||
{
|
||||
let jCard = this.jCard;
|
||||
jCard.set('n', [
|
||||
this.surName(),
|
||||
this.givenName(),
|
||||
this.middleName(),
|
||||
this.namePrefix(),
|
||||
this.nameSuffix()
|
||||
]/*, params, group*/);
|
||||
// jCard.parseFullName({set:true});
|
||||
|
||||
this.nickname() ? jCard.set('nickname', this.nickname()/*, params, group*/) : jCard.remove('nickname');
|
||||
|
||||
['email', 'tel', 'url'].forEach(field => {
|
||||
let values = this[field].map(item => item.value());
|
||||
jCard.get(field).forEach(prop => {
|
||||
let i = values.indexOf(prop.value);
|
||||
if (0 > i || !prop.value) {
|
||||
jCard.remove(prop);
|
||||
} else {
|
||||
values.splice(i, 1);
|
||||
}
|
||||
});
|
||||
values.forEach(value => value && jCard.add(field, value));
|
||||
});
|
||||
|
||||
// Done by server
|
||||
// jCard.set('rev', '2022-05-21T10:59:52Z')
|
||||
|
||||
return {
|
||||
Uid: this.id,
|
||||
jCard: JSON.stringify(jCard)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@ export class FolderModel extends AbstractModel {
|
|||
|
||||
canBeEdited: () => FolderType.User === folder.type() && folder.exists/* && folder.selectable()*/,
|
||||
|
||||
isSystemFolder: () => FolderType.User !== folder.type() | !!folder.kolabType(),
|
||||
isSystemFolder: () => FolderType.User !== folder.type() /*| !!folder.kolabType()*/,
|
||||
|
||||
canBeSelected: () => folder.selectable() && !folder.isSystemFolder(),
|
||||
|
||||
|
|
|
|||
|
|
@ -193,13 +193,6 @@
|
|||
height: 30px;
|
||||
}
|
||||
|
||||
.add-link {
|
||||
margin-left: 2px;
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
box-shadow: none;
|
||||
border-color: #fff;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import Remote from 'Remote/User/Fetch';
|
|||
|
||||
import { EmailModel } from 'Model/Email';
|
||||
import { ContactModel } from 'Model/Contact';
|
||||
import { ContactPropertyModel, ContactPropertyType } from 'Model/ContactProperty';
|
||||
|
||||
import { decorateKoCommands, showScreenPopup } from 'Knoin/Knoin';
|
||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
||||
|
|
@ -25,10 +24,9 @@ import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
|||
import { AskPopupView } from 'View/Popup/Ask';
|
||||
|
||||
const
|
||||
viewProperties = koArrayWithDestroy(),
|
||||
CONTACTS_PER_PAGE = 50,
|
||||
ScopeContacts = 'Contacts',
|
||||
propertyIsMail = prop => prop.isType(ContactPropertyType.Email),
|
||||
propertyIsName = prop => prop.isType(ContactPropertyType.FirstName) || prop.isType(ContactPropertyType.LastName);
|
||||
ScopeContacts = 'Contacts';
|
||||
|
||||
export class ContactsPopupView extends AbstractViewPopup {
|
||||
constructor() {
|
||||
|
|
@ -47,10 +45,8 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
|
||||
contactsPage: 1,
|
||||
|
||||
emptySelection: true,
|
||||
viewClearSearch: false,
|
||||
|
||||
viewID: '',
|
||||
viewReadOnly: false,
|
||||
|
||||
viewSaveTrigger: SaveSettingsStep.Idle,
|
||||
|
|
@ -58,13 +54,13 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
viewSaving: false,
|
||||
|
||||
watchDirty: false,
|
||||
watchHash: false
|
||||
watchHash: false,
|
||||
|
||||
contact: null
|
||||
});
|
||||
|
||||
this.contacts = ContactUserStore;
|
||||
|
||||
this.viewProperties = koArrayWithDestroy();
|
||||
|
||||
this.useCheckboxesInList = SettingsUserStore.useCheckboxesInList;
|
||||
|
||||
this.selector = new Selector(
|
||||
|
|
@ -78,19 +74,13 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
|
||||
this.selector.on('ItemSelect', contact => {
|
||||
this.populateViewContact(contact);
|
||||
if (!contact) {
|
||||
this.emptySelection(true);
|
||||
}
|
||||
});
|
||||
|
||||
this.selector.on('ItemGetUid', contact => contact ? contact.generateUid() : '');
|
||||
|
||||
this.bDropPageAfterDelete = false;
|
||||
|
||||
// this.saveCommandDebounce = this.saveCommand.bind(this).debounce(1000);
|
||||
|
||||
const
|
||||
// propertyFocused = property => !property.isValid() && !property.focused(),
|
||||
pagecount = () => Math.max(1, Math.ceil(this.contactsCount() / CONTACTS_PER_PAGE));
|
||||
|
||||
this.addComputables({
|
||||
|
|
@ -98,18 +88,6 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
|
||||
contactsPaginator: computedPaginatorHelper(this.contactsPage, pagecount),
|
||||
|
||||
viewPropertiesNames: () => this.viewProperties.filter(propertyIsName),
|
||||
|
||||
viewPropertiesEmails: () => this.viewProperties.filter(propertyIsMail),
|
||||
|
||||
viewPropertiesOther: () => this.viewProperties.filter(property => property.isType(ContactPropertyType.Nick)),
|
||||
|
||||
viewPropertiesWeb: () => this.viewProperties.filter(property => property.isType(ContactPropertyType.Web)),
|
||||
|
||||
viewPropertiesPhones: () => this.viewProperties.filter(property => property.isType(ContactPropertyType.Phone)),
|
||||
|
||||
contactHasValidName: () => !!this.viewProperties.find(prop => propertyIsName(prop) && prop.isValid()),
|
||||
|
||||
contactsCheckedOrSelected: () => {
|
||||
const checked = ContactUserStore.filter(item => item.checked && item.checked()),
|
||||
selected = this.currentContact();
|
||||
|
|
@ -119,11 +97,11 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
: checked;
|
||||
},
|
||||
|
||||
contactsCheckedOrSelectedUids: () => this.contactsCheckedOrSelected().map(contact => contact.id),
|
||||
contactsCheckedOrSelectedUids: () => this.contactsCheckedOrSelected().map(contact => contact.id()),
|
||||
|
||||
contactsSyncEnabled: () => ContactUserStore.allowSync() && ContactUserStore.syncMode(),
|
||||
|
||||
viewHash: () => '' + this.viewProperties.map(property => property.value && property.value()).join('')
|
||||
viewHash: () => '' + viewProperties.map(property => property.value && property.value()).join('')
|
||||
});
|
||||
|
||||
this.search.subscribe(() => this.reloadContactList());
|
||||
|
|
@ -134,12 +112,14 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
}
|
||||
});
|
||||
|
||||
this.saveCommand = this.saveCommand.bind(this);
|
||||
|
||||
decorateKoCommands(this, {
|
||||
// close: self => !self.watchDirty(),
|
||||
deleteCommand: self => 0 < self.contactsCheckedOrSelected().length,
|
||||
newMessageCommand: self => 0 < self.contactsCheckedOrSelected().length,
|
||||
saveCommand: self => !self.viewSaving() && !self.viewReadOnly()
|
||||
&& (self.contactHasValidName() || self.viewProperties.find(prop => propertyIsMail(prop) && prop.isValid())),
|
||||
&& (self.contact()?.hasValidName() || self.contact()?.email().length),
|
||||
syncCommand: self => !self.contacts.syncing() && !self.contacts.importing()
|
||||
});
|
||||
}
|
||||
|
|
@ -151,7 +131,6 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
|
||||
deleteCommand() {
|
||||
this.deleteSelectedContacts();
|
||||
this.emptySelection(true);
|
||||
}
|
||||
|
||||
newMessageCommand() {
|
||||
|
|
@ -213,22 +192,20 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
this.viewSaving(true);
|
||||
this.viewSaveTrigger(SaveSettingsStep.Animate);
|
||||
|
||||
const requestUid = Jua.randomId();
|
||||
const
|
||||
contact = this.contact(),
|
||||
requestUid = Jua.randomId();
|
||||
|
||||
Remote.request('ContactSave',
|
||||
(iError, oData) => {
|
||||
let res = false;
|
||||
this.viewSaving(false);
|
||||
|
||||
if (
|
||||
!iError &&
|
||||
oData.Result.RequestUid === requestUid &&
|
||||
0 < pInt(oData.Result.ResultID)
|
||||
if (!iError
|
||||
&& oData.Result.RequestUid === requestUid
|
||||
&& oData.Result.ResultID
|
||||
) {
|
||||
if (!this.viewID()) {
|
||||
this.viewID(pInt(oData.Result.ResultID));
|
||||
}
|
||||
|
||||
contact.id(oData.Result.ResultID);
|
||||
this.reloadContactList(); // TODO: remove when e-contact-foreach is dynamic
|
||||
res = true;
|
||||
}
|
||||
|
|
@ -243,8 +220,9 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
}
|
||||
}, {
|
||||
RequestUid: requestUid,
|
||||
Uid: this.viewID(),
|
||||
Properties: this.viewProperties.map(oItem => oItem.toJSON())
|
||||
Contact: contact
|
||||
// Uid: contact.id(),
|
||||
// jCard: contact.jCard
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -257,63 +235,6 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
});
|
||||
}
|
||||
|
||||
getPropertyPlaceholder(type) {
|
||||
let result = '';
|
||||
switch (type) {
|
||||
case ContactPropertyType.LastName:
|
||||
result = 'CONTACTS/PLACEHOLDER_ENTER_LAST_NAME';
|
||||
break;
|
||||
case ContactPropertyType.FirstName:
|
||||
result = 'CONTACTS/PLACEHOLDER_ENTER_FIRST_NAME';
|
||||
break;
|
||||
case ContactPropertyType.Nick:
|
||||
result = 'CONTACTS/PLACEHOLDER_ENTER_NICK_NAME';
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
addNewProperty(type, typeStr) {
|
||||
this.viewProperties.push(
|
||||
new ContactPropertyModel(type, typeStr || '', '', true, this.getPropertyPlaceholder(type))
|
||||
);
|
||||
}
|
||||
|
||||
addNewOrFocusProperty(type, typeStr) {
|
||||
const item = this.viewProperties.find(prop => prop.isType(type));
|
||||
if (item) {
|
||||
item.focused(true);
|
||||
} else {
|
||||
this.addNewProperty(type, typeStr);
|
||||
}
|
||||
}
|
||||
|
||||
addNewEmail() {
|
||||
this.addNewProperty(ContactPropertyType.Email, 'Home');
|
||||
}
|
||||
|
||||
addNewPhone() {
|
||||
this.addNewProperty(ContactPropertyType.Phone, 'Mobile');
|
||||
}
|
||||
|
||||
addNewWeb() {
|
||||
this.addNewProperty(ContactPropertyType.Web);
|
||||
}
|
||||
|
||||
addNewNickname() {
|
||||
this.addNewOrFocusProperty(ContactPropertyType.Nick);
|
||||
}
|
||||
|
||||
addNewNotes() {
|
||||
this.addNewOrFocusProperty(ContactPropertyType.Note);
|
||||
}
|
||||
|
||||
addNewBirthday() {
|
||||
this.addNewOrFocusProperty(ContactPropertyType.Birthday);
|
||||
}
|
||||
|
||||
exportVcf() {
|
||||
download(serverRequestRaw('ContactsVcf'), 'contacts.vcf');
|
||||
}
|
||||
|
|
@ -330,7 +251,7 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
|
||||
if (contacts.length) {
|
||||
contacts.forEach(contact => {
|
||||
if (currentContact && currentContact.id === contact.id) {
|
||||
if (currentContact && currentContact.id() === contact.id()) {
|
||||
currentContact = null;
|
||||
this.currentContact(null);
|
||||
}
|
||||
|
|
@ -366,33 +287,19 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
}
|
||||
}
|
||||
|
||||
removeProperty(oProp) {
|
||||
this.viewProperties.remove(oProp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?ContactModel} contact
|
||||
*/
|
||||
populateViewContact(contact) {
|
||||
let id = '';
|
||||
|
||||
this.watchHash(false);
|
||||
|
||||
this.emptySelection(false);
|
||||
this.viewReadOnly(false);
|
||||
|
||||
if (contact) {
|
||||
id = contact.id;
|
||||
this.viewReadOnly(!!contact.readOnly);
|
||||
} else {
|
||||
if (!contact) {
|
||||
contact = new ContactModel;
|
||||
contact.initDefaultProperties();
|
||||
}
|
||||
this.viewReadOnly(contact.readOnly());
|
||||
this.contact(contact);
|
||||
|
||||
this.viewID(id);
|
||||
|
||||
// this.viewProperties([]);
|
||||
this.viewProperties(contact.properties);
|
||||
viewProperties(contact.properties);
|
||||
|
||||
this.watchDirty(false);
|
||||
this.watchHash(true);
|
||||
|
|
@ -509,8 +416,8 @@ export class ContactsPopupView extends AbstractViewPopup {
|
|||
}
|
||||
|
||||
onHide() {
|
||||
this.contact(null);
|
||||
this.currentContact(null);
|
||||
this.emptySelection(true);
|
||||
this.search('');
|
||||
this.contactsCount(0);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue