Bug 1776129 - Add Custom1-4 fields to Address Book UI. r=aleca

This converts nsIAbCard style Custom[1-4] properties to X-Custom[1-4] vCard properties when a card is edited. Since the properties could exist in either form a number of hacks have been deployed.

Differential Revision: https://phabricator.services.mozilla.com/D153550

--HG--
extra : rebase_source : a793101ae36434240acca28e3892fe083520b170
This commit is contained in:
Geoff Lankow 2022-08-03 15:47:58 +12:00
Родитель f7a2b7878b
Коммит 2c2be3af27
13 изменённых файлов: 524 добавлений и 159 удалений

Просмотреть файл

@ -67,6 +67,11 @@ XPCOMUtils.defineLazyGetter(this, "SubDialog", function() {
}, },
}); });
}); });
XPCOMUtils.defineLazyGetter(this, "bundle", function() {
return Services.strings.createBundle(
"chrome://messenger/locale/addressbook/addressBook.properties"
);
});
UIDensity.registerWindow(window); UIDensity.registerWindow(window);
UIFontSize.registerWindow(window); UIFontSize.registerWindow(window);
@ -2398,10 +2403,7 @@ var detailsPane = {
let photoButton = document.getElementById("photoButton"); let photoButton = document.getElementById("photoButton");
// FIXME: Remove this once we get new strings after 102. // FIXME: Remove this once we get new strings after 102.
let stringBundle = Services.strings.createBundle( photoButton.title = bundle.GetStringFromName("browsePhoto");
"chrome://messenger/locale/addressbook/addressBook.properties"
);
photoButton.title = stringBundle.GetStringFromName("browsePhoto");
photoButton.addEventListener("click", () => { photoButton.addEventListener("click", () => {
if (this._photoDetails.sourceURL) { if (this._photoDetails.sourceURL) {
photoDialog.showWithURL( photoDialog.showWithURL(
@ -2954,6 +2956,28 @@ var detailsPane = {
time.setAttribute("tz", tz); time.setAttribute("tz", tz);
li.querySelector(".entry-value").appendChild(time); li.querySelector(".entry-value").appendChild(time);
} }
for (let key of ["Custom1", "Custom2", "Custom3", "Custom4"]) {
// Custom properties can be nsIAbCard properties or vCard properties.
// If there's both, the vCard property has precedence.
let value = card.getProperty(key, "");
if (card.supportsVCard) {
value =
card.vCardProperties.getFirstValue(`x-${key.toLowerCase()}`) ?? value;
}
if (value) {
let li = list.appendChild(createEntryItem());
li.querySelector(".entry-type").textContent = bundle.GetStringFromName(
`property${key}`
);
li.querySelector(".entry-type").style.setProperty(
"white-space",
"nowrap"
);
li.querySelector(".entry-value").textContent = value;
}
}
section.hidden = list.childElementCount == 0; section.hidden = list.childElementCount == 0;
this.isEditing = false; this.isEditing = false;
@ -3012,6 +3036,20 @@ var detailsPane = {
let card = this.currentCard; let card = this.currentCard;
if (card && card.supportsVCard) { if (card && card.supportsVCard) {
for (let key of ["Custom1", "Custom2", "Custom3", "Custom4"]) {
// Custom properties could still exist as nsIAbCard properties.
// If they do, and no vCard equivalent exists, add them to the vCard
// so that they get displayed.
let value = card.getProperty(key, "");
if (
value &&
card.vCardProperties.getFirstEntry(`x-${key.toLowerCase()}`) === null
) {
card.vCardProperties.addEntry(
new VCardPropertyEntry(`x-${key.toLowerCase()}`, {}, "text", value)
);
}
}
this.vCardEdit.vCardProperties = card.vCardProperties; this.vCardEdit.vCardProperties = card.vCardProperties;
// getProperty may return a "1" or "0" string, we want a boolean. // getProperty may return a "1" or "0" string, we want a boolean.
this.vCardEdit.preferDisplayName.checked = this.vCardEdit.preferDisplayName.checked =
@ -3059,13 +3097,10 @@ var detailsPane = {
*/ */
handleInvalidForm() { handleInvalidForm() {
// FIXME: Drop this in favor of an inline notification with fluent strings. // FIXME: Drop this in favor of an inline notification with fluent strings.
let stringBundle = Services.strings.createBundle(
"chrome://messenger/locale/addressbook/addressBook.properties"
);
Services.prompt.alert( Services.prompt.alert(
window, window,
stringBundle.GetStringFromName("cardRequiredDataMissingTitle"), bundle.GetStringFromName("cardRequiredDataMissingTitle"),
stringBundle.GetStringFromName("cardRequiredDataMissingMessage") bundle.GetStringFromName("cardRequiredDataMissingMessage")
); );
}, },
@ -3126,6 +3161,12 @@ var detailsPane = {
this.vCardEdit.preferDisplayName.checked this.vCardEdit.preferDisplayName.checked
); );
// By now, nsIAbCard custom properties should be on the vCard. Delete them.
card.deleteProperty("Custom1");
card.deleteProperty("Custom2");
card.deleteProperty("Custom3");
card.deleteProperty("Custom4");
// No photo or a new photo. Delete the old one. // No photo or a new photo. Delete the old one.
if (this._photoChanged) { if (this._photoChanged) {
let oldLeafName = card.getProperty("PhotoName", ""); let oldLeafName = card.getProperty("PhotoName", "");

Просмотреть файл

@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals VCardPropertyEntryView, vCardIdGen */
ChromeUtils.defineModuleGetter(
this,
"VCardPropertyEntry",
"resource:///modules/VCardUtils.jsm"
);
class VCardCustomComponent extends HTMLElement {
/** @type {VCardPropertyEntry[]} */
vCardPropertyEntries = null;
/** @type {HTMLInputElement[]} */
inputEls = null;
constructor() {
super();
let template = document.getElementById("template-vcard-edit-custom");
let clonedTemplate = template.content.cloneNode(true);
this.appendChild(clonedTemplate);
}
connectedCallback() {
if (this.isConnected) {
// FIXME: Add some Fluent strings so that we don't have to do this.
let stringBundle = Services.strings.createBundle(
"chrome://messenger/locale/addressbook/addressBook.properties"
);
this.inputEls = this.querySelectorAll("input");
let labelEls = this.querySelectorAll("label");
for (let i = 0; i < 4; i++) {
let inputId = vCardIdGen.next().value;
labelEls[i].textContent = stringBundle.GetStringFromName(
`propertyCustom${i + 1}`
);
labelEls[i].htmlFor = inputId;
this.inputEls[i].id = inputId;
}
this.fromVCardPropertyEntryToUI();
}
}
disconnectedCallback() {
if (!this.isConnected) {
this.inputEls = null;
this.vCardPropertyEntries = null;
}
}
fromVCardPropertyEntryToUI() {
for (let i = 0; i < 4; i++) {
this.inputEls[i].value = this.vCardPropertyEntries[i].value;
}
}
fromUIToVCardPropertyEntry() {
for (let i = 0; i < 4; i++) {
this.vCardPropertyEntries[i].value = this.inputEls[i].value;
}
}
}
customElements.define("vcard-custom", VCardCustomComponent);

Просмотреть файл

@ -2,11 +2,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals VCardAdrComponent, VCardEmailComponent, VCardIMPPComponent, /* globals VCardAdrComponent, VCardCustomComponent, VCardEmailComponent,
VCardNComponent, VCardFNComponent, VCardNickNameComponent, VCardIMPPComponent, VCardNComponent, VCardFNComponent,
VCardNoteComponent, VCardOrgComponent, VCardRoleComponent, VCardNickNameComponent, VCardNoteComponent, VCardOrgComponent,
VCardSpecialDateComponent, VCardTelComponent, VCardTitleComponent, VCardRoleComponent, VCardSpecialDateComponent, VCardTelComponent,
VCardTZComponent, VCardURLComponent */ VCardTitleComponent, VCardTZComponent, VCardURLComponent */
ChromeUtils.defineModuleGetter( ChromeUtils.defineModuleGetter(
this, this,
@ -106,6 +106,15 @@ class VCardEdit extends HTMLElement {
); );
} }
} }
for (let i = 1; i <= 4; i++) {
if (!this._vCardProperties.getFirstEntry(`x-custom${i}`)) {
this._vCardProperties.addEntry(
new VCardPropertyEntry(`x-custom${i}`, {}, "text", "")
);
}
}
this.updateView(); this.updateView();
} }
@ -123,13 +132,28 @@ class VCardEdit extends HTMLElement {
this.addFieldsetActions(); this.addFieldsetActions();
this._orgComponent = null;
// Insert the vCard property entries. // Insert the vCard property entries.
for (let vCardPropertyEntry of this.vCardProperties.entries) { for (let vCardPropertyEntry of this.vCardProperties.entries) {
this.insertVCardElement(vCardPropertyEntry, false); this.insertVCardElement(vCardPropertyEntry, false);
} }
let customProperties = ["x-custom1", "x-custom2", "x-custom3", "x-custom4"];
if (
customProperties.some(name => this.vCardProperties.getFirstValue(name))
) {
// If one of these properties has a value, display all of them.
let customFieldset = this.querySelector("#addr-book-edit-custom");
let customEl =
customFieldset.querySelector("vcard-custom") ||
new VCardCustomComponent();
customEl.vCardPropertyEntries = customProperties.map(name =>
this._vCardProperties.getFirstEntry(name)
);
let addCustom = document.getElementById("vcard-add-custom");
customFieldset.insertBefore(customEl, addCustom);
addCustom.hidden = true;
}
let nameEl = this.querySelector("vcard-n"); let nameEl = this.querySelector("vcard-n");
this.firstName = nameEl.firstNameEl.querySelector("input"); this.firstName = nameEl.firstNameEl.querySelector("input");
this.lastName = nameEl.lastNameEl.querySelector("input"); this.lastName = nameEl.lastNameEl.querySelector("input");
@ -538,6 +562,7 @@ class VCardEdit extends HTMLElement {
saveVCard() { saveVCard() {
for (let node of [ for (let node of [
...this.querySelectorAll("vcard-adr"), ...this.querySelectorAll("vcard-adr"),
...this.querySelectorAll("vcard-custom"),
...document.getElementById("vcard-email").children, ...document.getElementById("vcard-email").children,
...this.querySelectorAll("vcard-fn"), ...this.querySelectorAll("vcard-fn"),
...this.querySelectorAll("vcard-impp"), ...this.querySelectorAll("vcard-impp"),
@ -570,6 +595,13 @@ class VCardEdit extends HTMLElement {
) { ) {
emailEntries[0].params.pref = "1"; emailEntries[0].params.pref = "1";
} }
for (let i = 1; i <= 4; i++) {
let entry = this._vCardProperties.getFirstEntry(`x-custom${i}`);
if (!entry.value) {
this._vCardProperties.removeEntry(entry);
}
}
} }
/** /**
@ -707,6 +739,21 @@ class VCardEdit extends HTMLElement {
this.registerAddButton(addNote, "note", () => { this.registerAddButton(addNote, "note", () => {
addNote.hidden = true; addNote.hidden = true;
}); });
let addCustom = document.getElementById("vcard-add-custom");
addCustom.addEventListener("click", event => {
let el = new VCardCustomComponent();
el.vCardPropertyEntries = [
this._vCardProperties.getFirstEntry("x-custom1"),
this._vCardProperties.getFirstEntry("x-custom2"),
this._vCardProperties.getFirstEntry("x-custom3"),
this._vCardProperties.getFirstEntry("x-custom4"),
];
addCustom.parentNode.insertBefore(el, addCustom);
this.moveFocusIntoElement(el);
addCustom.hidden = true;
});
} }
/** /**

Просмотреть файл

@ -7,6 +7,7 @@
<!-- Scripts --> <!-- Scripts -->
<script src="chrome://messenger/content/addressbook/edit/adr.js"></script> <script src="chrome://messenger/content/addressbook/edit/adr.js"></script>
<script src="chrome://messenger/content/addressbook/edit/custom.js"></script>
<script src="chrome://messenger/content/addressbook/edit/org.js"></script> <script src="chrome://messenger/content/addressbook/edit/org.js"></script>
<script src="chrome://messenger/content/addressbook/edit/email.js"></script> <script src="chrome://messenger/content/addressbook/edit/email.js"></script>
<script src="chrome://messenger/content/addressbook/edit/fn.js"></script> <script src="chrome://messenger/content/addressbook/edit/fn.js"></script>
@ -126,6 +127,14 @@
class="addr-book-edit-fieldset-button" class="addr-book-edit-fieldset-button"
type="button"></button> type="button"></button>
</fieldset> </fieldset>
<!-- Custom -->
<fieldset id="addr-book-edit-custom" class="addr-book-edit-fieldset fieldset-reset">
<legend data-l10n-id="vcard-custom-header"/>
<button id="vcard-add-custom"
data-l10n-id="vcard-custom-add"
class="addr-book-edit-fieldset-button"
type="button"></button>
</fieldset>
</template> </template>
<!-- Individual fields --> <!-- Individual fields -->
@ -322,6 +331,26 @@
</div> </div>
</template> </template>
<!-- Custom -->
<template id="template-vcard-edit-custom">
<div class="vcard-adr-inputs">
<label for="custom1"/>
<input type="text"/>
</div>
<div class="vcard-adr-inputs">
<label for="custom2"/>
<input type="text"/>
</div>
<div class="vcard-adr-inputs">
<label for="custom3"/>
<input type="text"/>
</div>
<div class="vcard-adr-inputs">
<label for="custom4"/>
<input type="text"/>
</div>
</template>
<template id="template-vcard-edit-type"> <template id="template-vcard-edit-type">
<select class="vcard-type-selection"> <select class="vcard-type-selection">
<option value="work" data-l10n-id="vcard-entry-type-work"/> <option value="work" data-l10n-id="vcard-entry-type-work"/>

Просмотреть файл

@ -19,6 +19,7 @@ messenger.jar:
content/messenger/addressbook/abView-new.js (content/abView-new.js) content/messenger/addressbook/abView-new.js (content/abView-new.js)
# Edit view # Edit view
content/messenger/addressbook/edit/adr.js (content/vcard-edit/adr.js) content/messenger/addressbook/edit/adr.js (content/vcard-edit/adr.js)
content/messenger/addressbook/edit/custom.js (content/vcard-edit/custom.js)
content/messenger/addressbook/edit/edit.js (content/vcard-edit/edit.js) content/messenger/addressbook/edit/edit.js (content/vcard-edit/edit.js)
content/messenger/addressbook/edit/email.js (content/vcard-edit/email.js) content/messenger/addressbook/edit/email.js (content/vcard-edit/email.js)
content/messenger/addressbook/edit/fn.js (content/vcard-edit/fn.js) content/messenger/addressbook/edit/fn.js (content/vcard-edit/fn.js)

Просмотреть файл

@ -72,59 +72,6 @@ add_setup(async function() {
END:VCARD END:VCARD
`) `)
); );
// Card 3.
personalBook.addCard(
VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic3@invalid
ANNIVERSARY:2005
END:VCARD
`)
);
// Card 4.
personalBook.addCard(
VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic4@invalid
ANNIVERSARY:2006-06
END:VCARD
`)
);
// Card 5.
personalBook.addCard(
VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic5@invalid
ANNIVERSARY:--12
END:VCARD
`)
);
// Card 6.
personalBook.addCard(
VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic6@invalid
ANNIVERSARY;VALUE=DATE:--0704
END:VCARD
`)
);
// Card 7.
personalBook.addCard(
VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic7@invalid
ANNIVERSARY:---30
END:VCARD
`)
);
// Card 8.
personalBook.addCard(
VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
ORG:xbasic8org
END:VCARD
`)
);
MailServices.accounts.createLocalMailAccount(); MailServices.accounts.createLocalMailAccount();
let account = MailServices.accounts.accounts[0]; let account = MailServices.accounts.accounts[0];
@ -412,84 +359,6 @@ add_task(async function testDisplay() {
items[5].children[1].lastChild.getAttribute("tz"), items[5].children[1].lastChild.getAttribute("tz"),
"Pacific/Auckland" "Pacific/Auckland"
); );
// Test card 3:
EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(3), {}, abWindow);
await TestUtils.waitForCondition(() =>
BrowserTestUtils.is_visible(detailsPane)
);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "2005");
// Test card 4:
EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(4), {}, abWindow);
await TestUtils.waitForCondition(() =>
BrowserTestUtils.is_visible(detailsPane)
);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "June 2006");
// Test card 5:
EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(5), {}, abWindow);
await TestUtils.waitForCondition(() =>
BrowserTestUtils.is_visible(detailsPane)
);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "December");
// Test card 6:
EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(6), {}, abWindow);
await TestUtils.waitForCondition(() =>
BrowserTestUtils.is_visible(detailsPane)
);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "July 4");
// Test card 7:
EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(7), {}, abWindow);
await TestUtils.waitForCondition(() =>
BrowserTestUtils.is_visible(detailsPane)
);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "30");
// Test card 8:
EventUtils.synthesizeMouseAtCenter(cardsList.getRowAtIndex(8), {}, abWindow);
await TestUtils.waitForCondition(() =>
BrowserTestUtils.is_visible(detailsPane)
);
Assert.equal(viewContactName.textContent, "xbasic8org");
Assert.ok(BrowserTestUtils.is_hidden(selectedCardsSection)); Assert.ok(BrowserTestUtils.is_hidden(selectedCardsSection));
// Card 0, again, just to prove that everything was cleared properly. // Card 0, again, just to prove that everything was cleared properly.
@ -517,6 +386,161 @@ add_task(async function testDisplay() {
await closeAddressBookWindow(); await closeAddressBookWindow();
}); });
/**
* Test the display of dates with various components missing.
*/
add_task(async function testDates() {
let abWindow = await openAddressBookWindow();
let otherInfoSection = abWindow.document.getElementById("otherInfo");
// Year only.
let yearCard = await addAndDisplayCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic3@invalid
ANNIVERSARY:2005
END:VCARD
`);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
let items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "2005");
// Year and month.
let yearMonthCard = await addAndDisplayCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic4@invalid
ANNIVERSARY:2006-06
END:VCARD
`);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "June 2006");
// Month only.
let monthCard = await addAndDisplayCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic5@invalid
ANNIVERSARY:--12
END:VCARD
`);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "December");
// Month and day.
let monthDayCard = await addAndDisplayCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic6@invalid
ANNIVERSARY;VALUE=DATE:--0704
END:VCARD
`);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "July 4");
// Day only.
let dayCard = await addAndDisplayCard(formatVCard`
BEGIN:VCARD
EMAIL:xbasic7@invalid
ANNIVERSARY:---30
END:VCARD
`);
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 1);
Assert.equal(
items[0].children[0].dataset.l10nId,
"about-addressbook-entry-name-anniversary"
);
Assert.equal(items[0].children[1].textContent, "30");
await closeAddressBookWindow();
personalBook.deleteCards([
yearCard,
yearMonthCard,
monthCard,
monthDayCard,
dayCard,
]);
});
/**
* Only an organisation name.
*/
add_task(async function testOrganisationNameOnly() {
let card = await addAndDisplayCard(
VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
ORG:organisation
END:VCARD
`)
);
let abWindow = await getAddressBookWindow();
let viewContactName = abWindow.document.getElementById("viewContactName");
Assert.equal(viewContactName.textContent, "organisation");
await closeAddressBookWindow();
personalBook.deleteCards([card]);
});
/**
* Tests that custom properties (Custom1 etc.) are displayed.
*/
add_task(async function testCustomProperties() {
let card = VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
FN:custom person
X-CUSTOM3:x-custom three
X-CUSTOM4:x-custom four
END:VCARD
`);
card.setProperty("Custom2", "custom two");
card.setProperty("Custom4", "custom four");
card = await addAndDisplayCard(card);
let abWindow = await getAddressBookWindow();
let otherInfoSection = abWindow.document.getElementById("otherInfo");
Assert.ok(BrowserTestUtils.is_visible(otherInfoSection));
let items = otherInfoSection.querySelectorAll("li");
Assert.equal(items.length, 3);
// Custom 1 has no value, should not display.
// Custom 2 has an old property value, should display that.
Assert.equal(items[0].children[0].textContent, "Custom 2");
Assert.equal(items[0].children[1].textContent, "custom two");
// Custom 3 has a vCard property value, should display that.
Assert.equal(items[1].children[0].textContent, "Custom 3");
Assert.equal(items[1].children[1].textContent, "x-custom three");
// Custom 4 has both types of value, the vCard value should be displayed.
Assert.equal(items[2].children[0].textContent, "Custom 4");
Assert.equal(items[2].children[1].textContent, "x-custom four");
await closeAddressBookWindow();
personalBook.deleteCards([card]);
});
/** /**
* Checks that the edit button is hidden for read-only contacts. * Checks that the edit button is hidden for read-only contacts.
*/ */
@ -663,7 +687,7 @@ add_task(async function testReadOnlyActions() {
// Check contacts with All Address Books displayed. // Check contacts with All Address Books displayed.
openAllAddressBooks(); openAllAddressBooks();
Assert.equal(cardsList.view.rowCount, 12); Assert.equal(cardsList.view.rowCount, 6);
Assert.ok(BrowserTestUtils.is_hidden(detailsPane)); Assert.ok(BrowserTestUtils.is_hidden(detailsPane));
// Basic person from Personal Address Books. // Basic person from Personal Address Books.
@ -818,6 +842,30 @@ add_task(async function testGoogleEscaping() {
await promiseDirectoryRemoved(googleBook.URI); await promiseDirectoryRemoved(googleBook.URI);
}); });
async function addAndDisplayCard(card) {
if (typeof card == "string") {
card = VCardUtils.vCardToAbCard(card);
}
card = personalBook.addCard(card);
let abWindow = await openAddressBookWindow();
let abDocument = abWindow.document;
let cardsList = abDocument.getElementById("cards");
let detailsPane = abDocument.getElementById("detailsPane");
let index = cardsList.view.getIndexForUID(card.UID);
EventUtils.synthesizeMouseAtCenter(
cardsList.getRowAtIndex(index),
{},
abWindow
);
await TestUtils.waitForCondition(() =>
BrowserTestUtils.is_visible(detailsPane)
);
return card;
}
async function checkActionButtons( async function checkActionButtons(
primaryEmail, primaryEmail,
displayName, displayName,

Просмотреть файл

@ -184,6 +184,11 @@ function getFields(entryName, addIfNeeded = false, count) {
addButtonId = "vcard-add-org"; addButtonId = "vcard-add-org";
expectFocusSelector = "vcard-title:last-of-type input"; expectFocusSelector = "vcard-title:last-of-type input";
break; break;
case "custom":
fieldsSelector = "vcard-custom";
addButtonId = "vcard-add-custom";
expectFocusSelector = "vcard-custom:last-of-type input";
break;
default: default:
throw new Error("entryName not found"); throw new Error("entryName not found");
} }
@ -2827,6 +2832,72 @@ add_task(async function test_special_date_field() {
await closeAddressBookWindow(); await closeAddressBookWindow();
}); });
/**
* Tests that custom properties (Custom1 etc.) are editable.
*/
add_task(async function testCustomProperties() {
let card = VCardUtils.vCardToAbCard(formatVCard`
BEGIN:VCARD
FN:custom person
X-CUSTOM3:x-custom three
X-CUSTOM4:x-custom four
END:VCARD
`);
card.setProperty("Custom2", "custom two");
card.setProperty("Custom4", "custom four");
card = personalBook.addCard(card);
let abWindow = await openAddressBookWindow();
let abDocument = abWindow.document;
let cardsList = abDocument.getElementById("cards");
let detailsPane = abDocument.getElementById("detailsPane");
let editButton = abDocument.getElementById("editButton");
let saveEditButton = abDocument.getElementById("saveEditButton");
let index = cardsList.view.getIndexForUID(card.UID);
EventUtils.synthesizeMouseAtCenter(
cardsList.getRowAtIndex(index),
{},
abWindow
);
await TestUtils.waitForCondition(() =>
BrowserTestUtils.is_visible(detailsPane)
);
EventUtils.synthesizeMouseAtCenter(editButton, {}, abWindow);
await inEditingMode();
let customField = getFields("custom")[0];
let inputs = customField.querySelectorAll("input");
Assert.equal(inputs.length, 4);
Assert.equal(inputs[0].value, "");
Assert.equal(inputs[1].value, "custom two");
Assert.equal(inputs[2].value, "x-custom three");
Assert.equal(inputs[3].value, "x-custom four");
inputs[0].value = "x-custom one";
inputs[1].value = "x-custom two";
inputs[3].value = "";
EventUtils.synthesizeMouseAtCenter(saveEditButton, {}, abWindow);
await notInEditingMode(editButton);
card = personalBook.childCards.find(c => c.UID == card.UID);
checkCardValues(card, {
Custom2: null,
Custom4: null,
});
checkVCardValues(card, {
"x-custom1": [{ value: "x-custom one" }],
"x-custom2": [{ value: "x-custom two" }],
"x-custom3": [{ value: "x-custom three" }],
"x-custom4": [],
});
await closeAddressBookWindow();
personalBook.deleteCards([card]);
});
/** /**
* Tests that we correctly fix Google's bad escaping of colons in values, and * Tests that we correctly fix Google's bad escaping of colons in values, and
* other characters in URI values. * other characters in URI values.

Просмотреть файл

@ -146,3 +146,9 @@ vcard-org-title = Title
vcard-org-role = Role vcard-org-role = Role
vcard-org-org = Organization vcard-org-org = Organization
# Custom properties
vcard-custom-header = Custom Properties
vcard-custom-add = Add custom properties

Просмотреть файл

@ -505,6 +505,10 @@ var typeMap = {
"tel.cell": singleTextProperty("CellularNumber", "tel", { type: "cell" }), "tel.cell": singleTextProperty("CellularNumber", "tel", { type: "cell" }),
"url.work": singleTextProperty("WebPage1", "url", { type: "work" }, "url"), "url.work": singleTextProperty("WebPage1", "url", { type: "work" }, "url"),
"url.home": singleTextProperty("WebPage2", "url", { type: "home" }, "url"), "url.home": singleTextProperty("WebPage2", "url", { type: "home" }, "url"),
"x-custom1": singleTextProperty("Custom1", "x-custom1"),
"x-custom2": singleTextProperty("Custom2", "x-custom2"),
"x-custom3": singleTextProperty("Custom3", "x-custom3"),
"x-custom4": singleTextProperty("Custom4", "x-custom4"),
}; };
/** /**

Просмотреть файл

@ -12,5 +12,9 @@ FN:contact number two
NOTE:here's some unicode text… NOTE:here's some unicode text…
TITLE:"worker" TITLE:"worker"
N:two;contact;;; N:two;contact;;;
X-CUSTOM1;VALUE=TEXT:custom\, 1
X-CUSTOM2;VALUE=TEXT:custom 2
X-CUSTOM3;VALUE=TEXT:custom 3
X-CUSTOM4;VALUE=TEXT:custom\n4
UID:yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy UID:yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
END:VCARD END:VCARD

Просмотреть файл

@ -16,6 +16,9 @@ var { AppConstants } = ChromeUtils.import(
var { MailServices } = ChromeUtils.import( var { MailServices } = ChromeUtils.import(
"resource:///modules/MailServices.jsm" "resource:///modules/MailServices.jsm"
); );
var { VCardPropertyEntry } = ChromeUtils.import(
"resource:///modules/VCardUtils.jsm"
);
async function subtest(cardConstructor) { async function subtest(cardConstructor) {
let dirPrefId = MailServices.ab.newAddressBook( let dirPrefId = MailServices.ab.newAddressBook(
@ -35,6 +38,10 @@ async function subtest(cardConstructor) {
let contact2 = cardConstructor(); let contact2 = cardConstructor();
contact2.UID = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"; contact2.UID = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
contact2.setProperty("Custom1", "custom, 1");
contact2.setProperty("Custom2", "custom\t2");
contact2.setProperty("Custom3", "custom\r3");
contact2.setProperty("Custom4", "custom\n4");
contact2.displayName = "contact number two"; contact2.displayName = "contact number two";
contact2.firstName = "contact"; contact2.firstName = "contact";
contact2.lastName = "two"; contact2.lastName = "two";
@ -46,10 +53,6 @@ async function subtest(cardConstructor) {
contact2.setProperty("JobTitle", `"worker"`); contact2.setProperty("JobTitle", `"worker"`);
contact2.setProperty("Notes", "here's some unicode text…"); contact2.setProperty("Notes", "here's some unicode text…");
} }
contact2.setProperty("Custom1", "custom, 1");
contact2.setProperty("Custom2", "custom\t2");
contact2.setProperty("Custom3", "custom\r3");
contact2.setProperty("Custom4", "custom\n4");
contact2 = book.addCard(contact2); contact2 = book.addCard(contact2);
let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance( let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(

Просмотреть файл

@ -200,13 +200,17 @@ add_task(function testFromToPropertyMap() {
["LastName", "Test"], ["LastName", "Test"],
["FirstName", "Mike"], ["FirstName", "Mike"],
["PrimaryEmail", "mike@test.invalid"], ["PrimaryEmail", "mike@test.invalid"],
["Custom1", "custom one"],
["Custom2", "custom two"],
["Custom3", "custom three"],
["Custom4", "custom four"],
]; ];
let properties = VCardProperties.fromPropertyMap( let properties = VCardProperties.fromPropertyMap(
new Map(inProperties), new Map(inProperties),
"3.0" "3.0"
); );
Assert.equal(properties.entries.length, 4, "entry count"); Assert.equal(properties.entries.length, 8, "entry count");
propertyEqual( propertyEqual(
properties.getFirstEntry("version"), properties.getFirstEntry("version"),
{ {
@ -247,9 +251,49 @@ add_task(function testFromToPropertyMap() {
}, },
"email entry" "email entry"
); );
propertyEqual(
properties.getFirstEntry("x-custom1"),
{
name: "x-custom1",
params: {},
type: "text",
value: "custom one",
},
"custom1 entry"
);
propertyEqual(
properties.getFirstEntry("x-custom2"),
{
name: "x-custom2",
params: {},
type: "text",
value: "custom two",
},
"custom2 entry"
);
propertyEqual(
properties.getFirstEntry("x-custom3"),
{
name: "x-custom3",
params: {},
type: "text",
value: "custom three",
},
"custom3 entry"
);
propertyEqual(
properties.getFirstEntry("x-custom4"),
{
name: "x-custom4",
params: {},
type: "text",
value: "custom four",
},
"custom4 entry"
);
let outProperties = properties.toPropertyMap(); let outProperties = properties.toPropertyMap();
Assert.equal(outProperties.size, 4, "property count"); Assert.equal(outProperties.size, 8, "property count");
for (let [key, value] of inProperties) { for (let [key, value] of inProperties) {
Assert.equal(outProperties.get(key), value, `${key} property`); Assert.equal(outProperties.get(key), value, `${key} property`);
} }

Просмотреть файл

@ -31,12 +31,12 @@
"TEL;TYPE=cell;VALUE=TEXT:567-890-1234", "TEL;TYPE=cell;VALUE=TEXT:567-890-1234",
"URL;TYPE=work;VALUE=URL:http://127.0.0.1", "URL;TYPE=work;VALUE=URL:http://127.0.0.1",
"URL;TYPE=home;VALUE=URL:http://localhost", "URL;TYPE=home;VALUE=URL:http://localhost",
"X-CUSTOM1;VALUE=TEXT:Custom Field 1",
"X-CUSTOM2;VALUE=TEXT:Custom Field 2",
"X-CUSTOM3;VALUE=TEXT:Custom Field 3",
"X-CUSTOM4;VALUE=TEXT:Custom Field 4",
"UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
], ]
"Custom1": "Custom Field 1",
"Custom2": "Custom Field 2",
"Custom3": "Custom Field 3",
"Custom4": "Custom Field 4"
} }
], ],
"bug_263304": [ "bug_263304": [