Bug 1817682 - Functions for extensions to add custom column handlers. r=darktrojan
Differential Revision: https://phabricator.services.mozilla.com/D179632 --HG-- extra : amend_source : 5e8c2382b2823f0cbd581254cda378aa68722e17
This commit is contained in:
Родитель
6904135ab1
Коммит
861a5ccec8
|
@ -58,10 +58,9 @@ const messengerBundle = Services.strings.createBundle(
|
|||
"chrome://messenger/locale/messenger.properties"
|
||||
);
|
||||
|
||||
const { getDefaultColumns, getDefaultColumnsForCardsView, isOutgoing } =
|
||||
ChromeUtils.importESModule(
|
||||
"chrome://messenger/content/thread-pane-columns.mjs"
|
||||
);
|
||||
const { ThreadPaneColumns } = ChromeUtils.importESModule(
|
||||
"chrome://messenger/content/thread-pane-columns.mjs"
|
||||
);
|
||||
|
||||
// As defined in nsMsgDBView.h.
|
||||
const MSG_VIEW_FLAG_DUMMY = 0x20000000;
|
||||
|
@ -3955,9 +3954,9 @@ var threadPane = {
|
|||
*/
|
||||
isFirstScroll: true,
|
||||
|
||||
columns: getDefaultColumns(gFolder),
|
||||
columns: ThreadPaneColumns.getDefaultColumns(gFolder),
|
||||
|
||||
cardColumns: getDefaultColumnsForCardsView(gFolder),
|
||||
cardColumns: ThreadPaneColumns.getDefaultColumnsForCardsView(gFolder),
|
||||
|
||||
async init() {
|
||||
await quickFilterBar.init();
|
||||
|
@ -3966,6 +3965,9 @@ var threadPane = {
|
|||
Services.prefs.addObserver("mailnews.tags.", this);
|
||||
|
||||
Services.obs.addObserver(this, "addrbook-displayname-changed");
|
||||
Services.obs.addObserver(this, "custom-column-added");
|
||||
Services.obs.addObserver(this, "custom-column-removed");
|
||||
Services.obs.addObserver(this, "custom-column-refreshed");
|
||||
|
||||
threadTree = document.getElementById("threadTree");
|
||||
this.treeTable = threadTree.table;
|
||||
|
@ -4079,6 +4081,9 @@ var threadPane = {
|
|||
uninit() {
|
||||
Services.prefs.removeObserver("mailnews.tags.", this);
|
||||
Services.obs.removeObserver(this, "addrbook-displayname-changed");
|
||||
Services.obs.removeObserver(this, "custom-column-added");
|
||||
Services.obs.removeObserver(this, "custom-column-removed");
|
||||
Services.obs.removeObserver(this, "custom-column-refreshed");
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
|
@ -4132,12 +4137,25 @@ var threadPane = {
|
|||
}
|
||||
},
|
||||
observe(subject, topic, data) {
|
||||
if (topic == "nsPref:changed") {
|
||||
this.setUpTagStyles();
|
||||
} else if (topic == "addrbook-displayname-changed") {
|
||||
// This runs the when mail.displayname.version preference observer is
|
||||
// notified/the mail.displayname.version number has been updated.
|
||||
threadTree.invalidate();
|
||||
switch (topic) {
|
||||
case "nsPref:changed":
|
||||
this.setUpTagStyles();
|
||||
break;
|
||||
case "addrbook-displayname-changed":
|
||||
// This runs the when mail.displayname.version preference observer is
|
||||
// notified/the mail.displayname.version number has been updated.
|
||||
threadTree.invalidate();
|
||||
break;
|
||||
case "custom-column-refreshed":
|
||||
// TODO: Invalidate only the column identified by data.
|
||||
threadTree.invalidate();
|
||||
break;
|
||||
case "custom-column-added":
|
||||
this.addCustomColumn(data);
|
||||
break;
|
||||
case "custom-column-removed":
|
||||
this.removeCustomColumn(data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -4150,7 +4168,10 @@ var threadPane = {
|
|||
return;
|
||||
}
|
||||
|
||||
threadTree.classList.toggle("is-outgoing", isOutgoing(gFolder));
|
||||
threadTree.classList.toggle(
|
||||
"is-outgoing",
|
||||
ThreadPaneColumns.isOutgoing(gFolder)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -4830,7 +4851,7 @@ var threadPane = {
|
|||
restoreColumnsState() {
|
||||
// Always fetch a fresh array of columns for the cards view even if we don't
|
||||
// have a folder defined.
|
||||
this.cardColumns = getDefaultColumnsForCardsView(gFolder);
|
||||
this.cardColumns = ThreadPaneColumns.getDefaultColumnsForCardsView(gFolder);
|
||||
this.updateClassList();
|
||||
|
||||
// Avoid doing anything if no folder has been loaded yet.
|
||||
|
@ -4853,7 +4874,7 @@ var threadPane = {
|
|||
// default columns for the currently visible folder, otherwise the table
|
||||
// layout will maintain whatever state is currently set from the previous
|
||||
// folder, which it doesn't reflect reality.
|
||||
this.columns = getDefaultColumns(gFolder);
|
||||
this.columns = ThreadPaneColumns.getDefaultColumns(gFolder);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -4876,6 +4897,34 @@ var threadPane = {
|
|||
});
|
||||
},
|
||||
|
||||
makeCustomColumnCell(column) {
|
||||
if (!column?.custom) {
|
||||
throw new Error(`Not a custom column: ${column?.id}`);
|
||||
}
|
||||
|
||||
const cell = document.createElement("td");
|
||||
const columnName = column.id.toLowerCase();
|
||||
cell.classList.add(`${columnName}-column`);
|
||||
|
||||
// Default columns have this hardcoded in about3Pane.xhtml.
|
||||
cell.dataset.columnName = columnName;
|
||||
if (column.icon && column.iconCellDefinitions) {
|
||||
cell.classList.add("button-column");
|
||||
// Add predefined icons for custom icon columns.
|
||||
for (const { id, url, title, alt } of column.iconCellDefinitions) {
|
||||
const img = document.createElement("img");
|
||||
img.dataset.cellIconId = id;
|
||||
img.src = url;
|
||||
img.alt = alt || "";
|
||||
img.title = title || "";
|
||||
img.hidden = true;
|
||||
cell.appendChild(img);
|
||||
}
|
||||
}
|
||||
|
||||
return cell;
|
||||
},
|
||||
|
||||
/**
|
||||
* Force an update of the thread tree to reflect the columns change.
|
||||
*
|
||||
|
@ -4885,6 +4934,11 @@ var threadPane = {
|
|||
updateColumns(isSimple = false) {
|
||||
if (!this.rowTemplate) {
|
||||
this.rowTemplate = document.getElementById("threadPaneRowTemplate");
|
||||
this.rowTemplate.content.append(
|
||||
...ThreadPaneColumns.getCustomColumns().map(column =>
|
||||
this.makeCustomColumnCell(column)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update the row template to match the column properties.
|
||||
|
@ -4910,14 +4964,60 @@ var threadPane = {
|
|||
* Restore the default columns visibility and order and save the change.
|
||||
*/
|
||||
restoreDefaultColumns() {
|
||||
this.columns = getDefaultColumns(gFolder, gViewWrapper?.isSynthetic);
|
||||
this.cardColumns = getDefaultColumnsForCardsView(gFolder);
|
||||
this.columns = ThreadPaneColumns.getDefaultColumns(
|
||||
gFolder,
|
||||
gViewWrapper?.isSynthetic
|
||||
);
|
||||
this.cardColumns = ThreadPaneColumns.getDefaultColumnsForCardsView(gFolder);
|
||||
this.updateClassList();
|
||||
this.updateColumns();
|
||||
threadTree.reset();
|
||||
this.persistColumnStates();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a custom column to the thread pane.
|
||||
*
|
||||
* @param {string} columnID - uniqe id of the custom column
|
||||
*/
|
||||
addCustomColumn(columnID) {
|
||||
const column = ThreadPaneColumns.getColumn(columnID);
|
||||
if (this.rowTemplate) {
|
||||
this.rowTemplate.content.appendChild(this.makeCustomColumnCell(column));
|
||||
}
|
||||
|
||||
this.columns.push(column);
|
||||
const columnStates =
|
||||
gFolder.msgDatabase.dBFolderInfo.getCharProperty("columnStates");
|
||||
if (columnStates) {
|
||||
this.applyPersistedColumnsState(JSON.parse(columnStates));
|
||||
}
|
||||
|
||||
gViewWrapper?.dbView.addColumnHandler(column.id, column.handler);
|
||||
this.updateColumns();
|
||||
this.restoreSortIndicator();
|
||||
threadTree.reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a custom column from the thread pane.
|
||||
*
|
||||
* @param {string} columnID - uniqe id of the custom column
|
||||
*/
|
||||
removeCustomColumn(columnID) {
|
||||
if (this.rowTemplate) {
|
||||
this.rowTemplate.content
|
||||
.querySelector(`td.${columnID.toLowerCase()}-column`)
|
||||
?.remove();
|
||||
}
|
||||
|
||||
this.columns = this.columns.filter(column => column.id != columnID);
|
||||
this.updateColumns();
|
||||
|
||||
gViewWrapper?.dbView.removeColumnHandler(columnID);
|
||||
threadTree.reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shift the ordinal of a column by one based on the visible columns.
|
||||
*
|
||||
|
@ -4972,7 +5072,7 @@ var threadPane = {
|
|||
this.columns = data.columns;
|
||||
|
||||
this.persistColumnStates();
|
||||
this.updateColumns(true);
|
||||
this.updateColumns();
|
||||
threadTree.reset();
|
||||
},
|
||||
|
||||
|
@ -5038,25 +5138,30 @@ var threadPane = {
|
|||
* @param {object} data - The detail of the custom event.
|
||||
*/
|
||||
onSortChanged(data) {
|
||||
const sortColumn = sortController.convertSortTypeToColumnID(
|
||||
const curSortColumnId = sortController.convertSortTypeToColumnID(
|
||||
gViewWrapper.primarySortType
|
||||
);
|
||||
const column = data.column;
|
||||
const newSortColumnId = data.column;
|
||||
|
||||
// A click happened on the column that is already used to sort the list.
|
||||
if (sortColumn == column) {
|
||||
if (curSortColumnId == newSortColumnId) {
|
||||
if (gViewWrapper.isSortedAscending) {
|
||||
sortController.sortDescending();
|
||||
} else {
|
||||
sortController.sortAscending();
|
||||
}
|
||||
this.updateSortIndicator(column);
|
||||
this.updateSortIndicator(newSortColumnId);
|
||||
return;
|
||||
}
|
||||
|
||||
const sortName = this.columns.find(c => c.id == data.column).sortKey;
|
||||
// If the new sort column is a custom column, we need to pass the id of the
|
||||
// custom column, instead of the the general "byCustom" sortName.
|
||||
const newSortColumnData = this.columns.find(c => c.id == newSortColumnId);
|
||||
const sortName = newSortColumnData.custom
|
||||
? newSortColumnId
|
||||
: newSortColumnData.sortKey;
|
||||
sortController.sortThreadPane(sortName);
|
||||
this.updateSortIndicator(column);
|
||||
this.updateSortIndicator(newSortColumnId);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -5068,6 +5173,10 @@ var threadPane = {
|
|||
this.treeTable
|
||||
.querySelector(".sorting")
|
||||
?.classList.remove("sorting", "ascending", "descending");
|
||||
// The column could be a removed custom column.
|
||||
if (!column) {
|
||||
return;
|
||||
}
|
||||
this.treeTable
|
||||
.querySelector(`#${column} button`)
|
||||
?.classList.add(
|
||||
|
@ -5134,7 +5243,7 @@ var threadPane = {
|
|||
swappedColumnStateString = columStateString;
|
||||
}
|
||||
|
||||
const currentFolderIsOutgoing = isOutgoing(gFolder);
|
||||
const currentFolderIsOutgoing = ThreadPaneColumns.isOutgoing(gFolder);
|
||||
|
||||
/**
|
||||
* Update the columnStates property of the folder database and forget the
|
||||
|
@ -5149,7 +5258,7 @@ var threadPane = {
|
|||
// Check if the destination folder we're trying to update matches the same
|
||||
// special state of the folder we're getting the column state from.
|
||||
const colStateString =
|
||||
isOutgoing(folder) == currentFolderIsOutgoing
|
||||
ThreadPaneColumns.isOutgoing(folder) == currentFolderIsOutgoing
|
||||
? columStateString
|
||||
: swappedColumnStateString;
|
||||
|
||||
|
@ -5984,6 +6093,15 @@ customElements.whenDefined("tree-view-table-row").then(() => {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (column.custom && column.icon) {
|
||||
const images = cell.querySelectorAll("img");
|
||||
for (const image of images) {
|
||||
const cellIconProperty = `${column.id}-${image.dataset.cellIconId}`;
|
||||
image.hidden = !propertiesSet.has(cellIconProperty);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (textIndex >= 0) {
|
||||
if (isDummyRow) {
|
||||
cell.textContent = "";
|
||||
|
@ -6400,7 +6518,12 @@ var sortController = {
|
|||
this.sortThreadPane("byDate");
|
||||
},
|
||||
sortThreadPane(sortName) {
|
||||
const sortType = Ci.nsMsgViewSortType[sortName];
|
||||
// The sortName is either a sortKey (see Ci.nsMsgViewSortType and
|
||||
// DEFAULT_COLUMNS in thread-pane-columns.js), or the id of a custom column.
|
||||
// TODO: Using the columnId as input whould be a lot more obvious, here and
|
||||
// in gViewWrapper.sort(), because now sortType could be a string or an int.
|
||||
const sortType = Ci.nsMsgViewSortType[sortName] ?? sortName;
|
||||
|
||||
const grouped = gViewWrapper.showGroupedBySort;
|
||||
gViewWrapper._threadExpandAll = Boolean(
|
||||
gViewWrapper._viewFlags & Ci.nsMsgViewFlagsType.kExpandAll
|
||||
|
@ -6594,9 +6717,9 @@ var sortController = {
|
|||
break;
|
||||
case Ci.nsMsgViewSortType.byCustom:
|
||||
// TODO: either change try() catch to if (property exists) or restore
|
||||
// the getColumnHandler() check.
|
||||
// the ThreadPaneColumns.getColumnHandler() check.
|
||||
try {
|
||||
// getColumnHandler throws an error when the ID is not handled
|
||||
// ThreadPaneColumns.getColumnHandler throws an error when the ID is not handled
|
||||
columnID = gDBView.curCustomColumn;
|
||||
} catch (e) {
|
||||
// error - means no handler
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
// msgViewNavigation.js
|
||||
/* globals CrossFolderNavigation */
|
||||
|
||||
// about3pane.js
|
||||
/* globals ThreadPaneColumns */
|
||||
|
||||
var { MailServices } = ChromeUtils.import(
|
||||
"resource:///modules/MailServices.jsm"
|
||||
);
|
||||
|
@ -980,6 +983,9 @@ var dbViewWrapperListener = {
|
|||
onSearching(isSearching) {},
|
||||
onCreatedView() {
|
||||
if (window.threadTree) {
|
||||
for (const col of ThreadPaneColumns.getCustomColumns()) {
|
||||
gViewWrapper.dbView.addColumnHandler(col.id, col.handler);
|
||||
}
|
||||
window.threadPane.setTreeView(gViewWrapper.dbView);
|
||||
window.threadPane.isFirstScroll = true;
|
||||
window.threadPane.scrollDetected = false;
|
||||
|
|
|
@ -266,7 +266,7 @@ const DEFAULT_COLUMNS = [
|
|||
* @param {nsIMsgFolder} folder - The message folder.
|
||||
* @returns {boolean} True if the folder is Outgoing.
|
||||
*/
|
||||
export const isOutgoing = folder => {
|
||||
const isOutgoing = folder => {
|
||||
return folder.isSpecialFolder(
|
||||
lazy.DBViewWrapper.prototype.OUTGOING_FOLDER_FLAGS,
|
||||
true
|
||||
|
@ -283,7 +283,7 @@ export const isOutgoing = folder => {
|
|||
* the gloda results list.
|
||||
* @returns {object[]}
|
||||
*/
|
||||
export function getDefaultColumns(folder, isSynthetic = false) {
|
||||
function getDefaultColumns(folder, isSynthetic = false) {
|
||||
// Create a clone we can edit.
|
||||
const updatedColumns = DEFAULT_COLUMNS.map(column => ({ ...column }));
|
||||
|
||||
|
@ -379,7 +379,206 @@ function getProperSenderForCardsView(folder) {
|
|||
* @param {?nsIMsgFolder} folder - The currently viewed folder if available.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getDefaultColumnsForCardsView(folder) {
|
||||
function getDefaultColumnsForCardsView(folder) {
|
||||
const sender = getProperSenderForCardsView(folder);
|
||||
return ["subjectCol", sender, "dateCol", "tagsCol", "totalCol"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef CustomColumnProperties
|
||||
* @property {string} name - Name of the column as displayed in the column
|
||||
* header of text columns, and in the column picker menu.
|
||||
* @property {boolean} [hidden] - Whether the column should be initially hidden.
|
||||
* @property {boolean} [icon] - Whether the column is an icon column.
|
||||
* @property {IconCellDefinition[]} [iconCellDefinitions] - Cell icon definitions
|
||||
* for the column. Required if the icon property is set.
|
||||
* @property {string} [iconHeaderUrl] - Header icon url for the column.
|
||||
* Required if the icon property is set.
|
||||
* @property {boolean} [resizable] - Whether the column should be resizable.
|
||||
* @property {boolean} [sortable] - Whether the column should be sortable.
|
||||
*
|
||||
* @property {TextCallback} textCallback - Callback function to retrieve the
|
||||
* text to be used for a given msgHdr. Used for sorting if no dedicated
|
||||
* sortCallback function given. Also used as display name if columns are
|
||||
* grouped by the sorted column.
|
||||
* @property {IconCallback} [iconCallback] - Callback function to retrieve the
|
||||
* icon id to be used for a given msgHdr. Required if icon property is set.
|
||||
* @property {SortCallback} [sortCallback] - Callback function to retrieve a
|
||||
* numeric sort key for a given msgHdr. If not given, column will be sorted
|
||||
* by the value returned by specified textCallback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef IconCellDefinition
|
||||
* @property {string} id - The id of the icon. Must be alphanumeric only.
|
||||
* @property {string} url - The url of the icon.
|
||||
* @property {string} [title] - Optional value for the icon's title attribute.
|
||||
* @property {string} [alt] - Optional value for the icon's alt attribute.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Callback function to retrieve the icon to be used for the given msgHdr.
|
||||
* @callback IconCallback
|
||||
* @param {nsIMsgDBHdr} msgHdr
|
||||
*
|
||||
* @returns {string} The id of the icon to be used, as specified in the
|
||||
* iconCellDefinitions property.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Callback function to retrieve a numeric sort key for the given msgHdr.
|
||||
* @callback SortCallback
|
||||
* @param {nsIMsgDBHdr} msgHdr
|
||||
*
|
||||
* @returns {integer} A numeric sort key.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Callback function to retrieve the text to be used for the given msgHdr.
|
||||
* @callback TextCallback
|
||||
* @param {nsIMsgDBHdr} msgHdr
|
||||
*
|
||||
* @returns {string} The text content.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Register a custom column.
|
||||
*
|
||||
* @param {string} id - uniqe id of the custom column
|
||||
* @param {CustomColumnProperties} properties
|
||||
*/
|
||||
function addCustomColumn(id, properties) {
|
||||
const {
|
||||
name: columnName,
|
||||
resizable = true,
|
||||
hidden = false,
|
||||
icon = false,
|
||||
sortable = false,
|
||||
iconCellDefinitions = [],
|
||||
iconHeaderUrl = "",
|
||||
iconCallback = null,
|
||||
sortCallback = null,
|
||||
textCallback = null,
|
||||
} = properties;
|
||||
|
||||
if (DEFAULT_COLUMNS.some(column => column.id == id)) {
|
||||
throw new Error(`Cannot add custom column, id is already used: ${id}`);
|
||||
}
|
||||
if (!columnName) {
|
||||
throw new Error(`Missing name property for custom column: ${id}`);
|
||||
}
|
||||
if (icon) {
|
||||
if (!iconCellDefinitions || !iconHeaderUrl || !iconCallback) {
|
||||
throw new Error(`Invalid icon properties for custom icon column: ${id}`);
|
||||
}
|
||||
if (iconCellDefinitions.some(e => !e.id || !e.url || /\W/g.test(e.id))) {
|
||||
throw new Error(
|
||||
`Invalid icon definition: ${JSON.stringify(iconCellDefinitions)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!textCallback) {
|
||||
throw new Error(`Missing textCallback property for custom column: ${id}`);
|
||||
}
|
||||
|
||||
const columnDef = {
|
||||
id,
|
||||
name: columnName,
|
||||
ordinal: DEFAULT_COLUMNS.length + 1,
|
||||
resizable,
|
||||
hidden,
|
||||
icon,
|
||||
sortable,
|
||||
custom: true,
|
||||
iconCellDefinitions,
|
||||
iconHeaderUrl,
|
||||
handler: {
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIMsgCustomColumnHandler"]),
|
||||
getCellText: textCallback,
|
||||
// With Bug 1192696, Grouped By Sort was implemented for custom columns.
|
||||
// Implementers should consider that the value returned by GetSortStringForRow
|
||||
// will be displayed in the grouped header row, as well as be used as the
|
||||
// sort string.
|
||||
getSortStringForRow: textCallback,
|
||||
// Allow to provide a dedicated numerical sort function.
|
||||
getSortLongForRow: sortCallback,
|
||||
isString() {
|
||||
return !sortCallback;
|
||||
},
|
||||
getRowProperties(msgHdr) {
|
||||
// Row properties are used for icons.
|
||||
if (icon) {
|
||||
const iconId = iconCallback(msgHdr);
|
||||
if (/\W/g.test(iconId)) {
|
||||
throw new Error(`Invalid icon value used: ${iconId}`);
|
||||
}
|
||||
return `${id}-${iconId}`;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
};
|
||||
DEFAULT_COLUMNS.push(columnDef);
|
||||
|
||||
Services.obs.notifyObservers(null, "custom-column-added", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a custom column.
|
||||
*
|
||||
* @param {string} id - uniqe id of the custom column
|
||||
*/
|
||||
function removeCustomColumn(id) {
|
||||
const index = DEFAULT_COLUMNS.findIndex(column => column.id == id);
|
||||
if (index >= 0) {
|
||||
DEFAULT_COLUMNS.splice(index, 1);
|
||||
}
|
||||
|
||||
Services.obs.notifyObservers(null, "custom-column-removed", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh display of a custom column.
|
||||
*
|
||||
* @param {string} id - uniqe id of the custom column
|
||||
*/
|
||||
function refreshCustomColumn(id) {
|
||||
Services.obs.notifyObservers(null, "custom-column-refreshed", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the registered column information for the column with the given id.
|
||||
*
|
||||
* @param {string} id - uniqe id of the custom column
|
||||
* @returns {object} Entry of the DEFAULT_COLUMNS array with the given id, or null.
|
||||
*/
|
||||
function getColumn(id) {
|
||||
const columnDef = DEFAULT_COLUMNS.find(column => column.id == id);
|
||||
if (!columnDef) {
|
||||
console.warn(`Found unknown column ${id}`);
|
||||
return null;
|
||||
}
|
||||
return { ...columnDef };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the registered column information of all custom columns.
|
||||
*
|
||||
* @returns {object} Entries of the DEFAULT_COLUMNS array of custom columns.
|
||||
*/
|
||||
function getCustomColumns() {
|
||||
return DEFAULT_COLUMNS.filter(column => column.custom).map(column => ({
|
||||
...column,
|
||||
}));
|
||||
}
|
||||
|
||||
export const ThreadPaneColumns = {
|
||||
isOutgoing,
|
||||
getDefaultColumns,
|
||||
getDefaultColumnsForCardsView,
|
||||
addCustomColumn,
|
||||
removeCustomColumn,
|
||||
refreshCustomColumn,
|
||||
getColumn,
|
||||
getCustomColumns,
|
||||
};
|
||||
|
|
|
@ -2228,6 +2228,8 @@ class TreeViewTableHeaderCell extends HTMLTableCellElement {
|
|||
|
||||
if (column.l10n?.header) {
|
||||
document.l10n.setAttributes(this.#button, column.l10n.header);
|
||||
} else if (column.name && !column.icon) {
|
||||
this.#button.textContent = column.name;
|
||||
}
|
||||
|
||||
// Add an image if this is a table header that needs to display an icon,
|
||||
|
@ -2235,7 +2237,7 @@ class TreeViewTableHeaderCell extends HTMLTableCellElement {
|
|||
if (column.icon) {
|
||||
this.dataset.type = "icon";
|
||||
const img = document.createElement("img");
|
||||
img.src = "";
|
||||
img.src = column.custom && column.icon ? column.iconHeaderUrl : "";
|
||||
img.alt = "";
|
||||
this.#button.appendChild(img);
|
||||
}
|
||||
|
@ -2464,6 +2466,8 @@ class TreeViewTableColumnPicker extends HTMLTableCellElement {
|
|||
menuitem.setAttribute("closemenu", "none");
|
||||
if (column.l10n?.menuitem) {
|
||||
document.l10n.setAttributes(menuitem, column.l10n.menuitem);
|
||||
} else if (column.name) {
|
||||
menuitem.label = column.name;
|
||||
}
|
||||
|
||||
menuitem.addEventListener("command", () => {
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/* 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/. */
|
||||
|
||||
const { ThreadPaneColumns } = ChromeUtils.importESModule(
|
||||
"chrome://messenger/content/thread-pane-columns.mjs"
|
||||
);
|
||||
|
||||
add_task(function testGetters() {
|
||||
const defaultColumns = ThreadPaneColumns.getDefaultColumns();
|
||||
const defaultSubjectColumn = defaultColumns.find(
|
||||
column => column.id == "subjectCol"
|
||||
);
|
||||
|
||||
Assert.notEqual(
|
||||
ThreadPaneColumns.getDefaultColumns().find(
|
||||
column => column.id == "subjectCol"
|
||||
),
|
||||
defaultSubjectColumn,
|
||||
"ThreadPaneColumns.getDefaultColumns should return different objects for each call"
|
||||
);
|
||||
|
||||
const subjectColumn1 = ThreadPaneColumns.getColumn("subjectCol");
|
||||
Assert.notEqual(
|
||||
subjectColumn1,
|
||||
defaultSubjectColumn,
|
||||
"ThreadPaneColumns.getColumn and ThreadPaneColumns.getDefaultColumns should return different objects"
|
||||
);
|
||||
|
||||
const subjectColumn2 = ThreadPaneColumns.getColumn("subjectCol");
|
||||
Assert.notEqual(
|
||||
subjectColumn1,
|
||||
subjectColumn2,
|
||||
"ThreadPaneColumns.getColumn should return different objects for each call"
|
||||
);
|
||||
|
||||
subjectColumn1.testProperty = "hello!";
|
||||
Assert.ok(
|
||||
!defaultSubjectColumn.testProperty,
|
||||
"property changes should not affect other instances of the same column"
|
||||
);
|
||||
Assert.ok(
|
||||
!subjectColumn2.testProperty,
|
||||
"property changes should not affect other instances of the same column"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(function testCustomColumns() {
|
||||
Assert.deepEqual(
|
||||
ThreadPaneColumns.getCustomColumns(),
|
||||
[],
|
||||
"should be no custom columns"
|
||||
);
|
||||
Assert.equal(
|
||||
ThreadPaneColumns.getColumn("testCol"),
|
||||
null,
|
||||
"ThreadPaneColumns.getColumn should return null"
|
||||
);
|
||||
|
||||
ThreadPaneColumns.addCustomColumn("testCol", {
|
||||
name: "Test Column",
|
||||
textCallback: () => {
|
||||
"static value";
|
||||
},
|
||||
});
|
||||
|
||||
const testColumn = ThreadPaneColumns.getColumn("testCol");
|
||||
Assert.equal(testColumn.id, "testCol", "Column should have the correct id");
|
||||
Assert.equal(
|
||||
testColumn.name,
|
||||
"Test Column",
|
||||
"Column should have the correct name"
|
||||
);
|
||||
Assert.ok(testColumn.custom, "Column should be a custom column");
|
||||
Assert.ok(!testColumn.sortable, "Column should be a sortable");
|
||||
Assert.ok(
|
||||
testColumn.handler.QueryInterface(Ci.nsIMsgCustomColumnHandler),
|
||||
"Column handler should be a custom column handler"
|
||||
);
|
||||
|
||||
const customColumns = ThreadPaneColumns.getCustomColumns();
|
||||
Assert.equal(
|
||||
customColumns.length,
|
||||
1,
|
||||
"should return a single custom column object"
|
||||
);
|
||||
Assert.notEqual(
|
||||
customColumns[0],
|
||||
testColumn,
|
||||
"should return a complex test column object, a simple equal compare should fail"
|
||||
);
|
||||
Assert.deepEqual(
|
||||
customColumns[0],
|
||||
testColumn,
|
||||
"should return the correct test column object"
|
||||
);
|
||||
|
||||
ThreadPaneColumns.removeCustomColumn("testCol");
|
||||
|
||||
Assert.deepEqual(
|
||||
ThreadPaneColumns.getCustomColumns(),
|
||||
[],
|
||||
"should find no custom columns"
|
||||
);
|
||||
Assert.equal(
|
||||
ThreadPaneColumns.getColumn("testCol"),
|
||||
null,
|
||||
"ThreadPaneColumns.getColumn should return null"
|
||||
);
|
||||
});
|
|
@ -5,6 +5,7 @@ support-files = distribution.ini resources/*
|
|||
|
||||
[test_alertHook.js]
|
||||
[test_attachmentChecker.js]
|
||||
[test_columns.js]
|
||||
[test_bug1086527.js]
|
||||
[test_devtools_url.js]
|
||||
[test_emptyTrash_dbViewWrapper.js]
|
||||
|
|
|
@ -1728,6 +1728,8 @@ DBViewWrapper.prototype = {
|
|||
if (sortTypeType != "number") {
|
||||
sortCustomColumn = sortTypeType == "string" ? sortType : sortType.id;
|
||||
sortType = Ci.nsMsgViewSortType.byCustom;
|
||||
// Set correct sortType.
|
||||
this._sort[aIndex][0] = sortType;
|
||||
}
|
||||
|
||||
return [sortType, sortOrder, sortCustomColumn];
|
||||
|
|
|
@ -36,6 +36,10 @@ var { GlodaSyntheticView } = ChromeUtils.import(
|
|||
"resource:///modules/gloda/GlodaSyntheticView.jsm"
|
||||
);
|
||||
|
||||
var { ThreadPaneColumns } = ChromeUtils.importESModule(
|
||||
"chrome://messenger/content/thread-pane-columns.mjs"
|
||||
);
|
||||
|
||||
var folderInbox, folderSent, folderVirtual, folderA, folderB;
|
||||
// INBOX_DEFAULTS sans 'dateCol' but gains 'tagsCol'
|
||||
var columnsB;
|
||||
|
@ -100,7 +104,6 @@ add_setup(async function () {
|
|||
useCorrespondent ? "correspondentCol" : "senderCol",
|
||||
"junkStatusCol",
|
||||
"dateCol",
|
||||
"locationCol",
|
||||
];
|
||||
GLODA_DEFAULTS = [
|
||||
"threadCol",
|
||||
|
@ -147,12 +150,18 @@ function assert_visible_columns(desiredColumns) {
|
|||
const visibleColumns = columns
|
||||
.filter(column => !column.hidden)
|
||||
.map(column => column.id);
|
||||
const failCol = visibleColumns.filter(x => !desiredColumns.includes(x));
|
||||
let failCol = visibleColumns.filter(x => !desiredColumns.includes(x));
|
||||
if (failCol.length) {
|
||||
throw new Error(
|
||||
`Found unexpected visible columns: '${failCol}'!\ndesired list: ${desiredColumns}\nactual list: ${visibleColumns}`
|
||||
);
|
||||
}
|
||||
failCol = desiredColumns.filter(x => !visibleColumns.includes(x));
|
||||
if (failCol.length) {
|
||||
throw new Error(
|
||||
`Found unexpected hidden columns: '${failCol}'!\ndesired list: ${desiredColumns}\nactual list: ${visibleColumns}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -530,7 +539,7 @@ add_task(async function test_column_reordering_persists() {
|
|||
close_tab(tabB);
|
||||
});
|
||||
|
||||
async function invoke_column_picker_option(aActions) {
|
||||
async function open_column_picker() {
|
||||
const tabmail = document.getElementById("tabmail");
|
||||
const about3Pane = tabmail.currentAbout3Pane;
|
||||
|
||||
|
@ -547,6 +556,12 @@ async function invoke_column_picker_option(aActions) {
|
|||
);
|
||||
EventUtils.synthesizeMouseAtCenter(colPicker, {}, about3Pane);
|
||||
await shownPromise;
|
||||
|
||||
return colPickerPopup;
|
||||
}
|
||||
|
||||
async function invoke_column_picker_option(aActions) {
|
||||
const colPickerPopup = await open_column_picker();
|
||||
await click_menus_in_sequence(colPickerPopup, aActions);
|
||||
}
|
||||
|
||||
|
@ -569,6 +584,64 @@ add_task(async function test_reset_to_inbox() {
|
|||
assert_visible_columns(INBOX_DEFAULTS);
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers a custom column and verifies it is added to the thread pane.
|
||||
*/
|
||||
add_task(async function test_custom_columns() {
|
||||
await enter_folder(inboxFolder);
|
||||
assert_visible_columns(INBOX_DEFAULTS);
|
||||
|
||||
ThreadPaneColumns.addCustomColumn("testCol", {
|
||||
name: "Test",
|
||||
sortCallback(header) {
|
||||
return header.subject.length;
|
||||
},
|
||||
textCallback(header) {
|
||||
return header.subject.length;
|
||||
},
|
||||
});
|
||||
await new Promise(setTimeout);
|
||||
|
||||
assert_visible_columns(INBOX_DEFAULTS);
|
||||
|
||||
let colPickerPopup = await open_column_picker();
|
||||
let columnItem = colPickerPopup.querySelector(
|
||||
`menuitem[type="checkbox"][value="testCol"]`
|
||||
);
|
||||
Assert.ok(columnItem, "Column item should exist");
|
||||
Assert.ok(
|
||||
!columnItem.hasAttribute("checked"),
|
||||
"Column item should not be checked"
|
||||
);
|
||||
colPickerPopup.hidePopup();
|
||||
|
||||
await toggleColumn("testCol");
|
||||
assert_visible_columns([...INBOX_DEFAULTS, "testCol"]);
|
||||
|
||||
colPickerPopup = await open_column_picker();
|
||||
columnItem = colPickerPopup.querySelector(
|
||||
`menuitem[type="checkbox"][value="testCol"]`
|
||||
);
|
||||
Assert.ok(columnItem, "Column item should exist");
|
||||
Assert.equal(
|
||||
columnItem.getAttribute("checked"),
|
||||
"true",
|
||||
"Column item should be checked"
|
||||
);
|
||||
colPickerPopup.hidePopup();
|
||||
|
||||
ThreadPaneColumns.removeCustomColumn("testCol");
|
||||
|
||||
assert_visible_columns(INBOX_DEFAULTS);
|
||||
|
||||
colPickerPopup = await open_column_picker();
|
||||
columnItem = colPickerPopup.querySelector(
|
||||
`menuitem[type="checkbox"][value="testCol"]`
|
||||
);
|
||||
Assert.ok(!columnItem, "Column item should not exist");
|
||||
colPickerPopup.hidePopup();
|
||||
});
|
||||
|
||||
async function _apply_to_folder_common(aChildrenToo, folder) {
|
||||
let notificatonPromise;
|
||||
if (aChildrenToo) {
|
||||
|
|
|
@ -3,18 +3,21 @@
|
|||
* 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/. */
|
||||
|
||||
#include "nsITreeView.idl"
|
||||
#include "nsISupports.idl"
|
||||
|
||||
interface nsIMsgDBHdr;
|
||||
|
||||
/* //TODO JavaDoc
|
||||
When implementing a js custom column handler (of type nsITreeView) you must implement the following
|
||||
functions:
|
||||
When implementing a custom column handler, the following methods are
|
||||
currently not supported:
|
||||
1. isEditable
|
||||
2. GetCellProperties
|
||||
3. GetImageSrc
|
||||
4. GetCellText
|
||||
5. CycleCell
|
||||
4. CycleCell
|
||||
|
||||
The following methods of the nsIMsgCustomColumnHandler must be
|
||||
implemented:
|
||||
5. GetCellText
|
||||
6. GetSortStringForRow
|
||||
7. GetSortLongForRow
|
||||
8. isString
|
||||
|
@ -26,14 +29,12 @@ interface nsIMsgDBHdr;
|
|||
Implementers should consider that the value returned by GetSortStringForRow
|
||||
will be displayed in the grouped header row, as well as be used as the
|
||||
sort string.
|
||||
|
||||
If implementing a c++ custom column handler, you must define all
|
||||
nsITreeView and nsIMsgCustomColumnHandler methods.
|
||||
*/
|
||||
|
||||
[scriptable, uuid(00f75b13-3ac4-4a17-a8b9-c6e4dd1b3f32)]
|
||||
interface nsIMsgCustomColumnHandler : nsITreeView
|
||||
{
|
||||
interface nsIMsgCustomColumnHandler : nsISupports {
|
||||
AString getRowProperties(in nsIMsgDBHdr aHdr);
|
||||
AString getCellText(in nsIMsgDBHdr aHdr);
|
||||
AString getSortStringForRow(in nsIMsgDBHdr aHdr);
|
||||
unsigned long getSortLongForRow(in nsIMsgDBHdr aHdr);
|
||||
boolean isString();
|
||||
|
|
|
@ -943,16 +943,6 @@ NS_IMETHODIMP
|
|||
nsMsgDBView::IsEditable(int32_t row, nsTreeColumn* col, bool* _retval) {
|
||||
NS_ENSURE_ARG_POINTER(col);
|
||||
NS_ENSURE_ARG_POINTER(_retval);
|
||||
// Attempt to retrieve a custom column handler. If it exists call it and
|
||||
// return.
|
||||
const nsAString& colID = col->GetId();
|
||||
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
|
||||
|
||||
if (colHandler) {
|
||||
colHandler->IsEditable(row, col, _retval);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
*_retval = false;
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -1161,7 +1151,7 @@ nsMsgDBView::GetRowProperties(int32_t index, nsAString& properties) {
|
|||
// Give the custom column handlers a chance to style the row.
|
||||
for (int i = 0; i < m_customColumnHandlers.Count(); i++) {
|
||||
nsString extra;
|
||||
m_customColumnHandlers[i]->GetRowProperties(index, extra);
|
||||
m_customColumnHandlers[i]->GetRowProperties(msgHdr, extra);
|
||||
if (!extra.IsEmpty()) {
|
||||
properties.Append(' ');
|
||||
properties.Append(extra);
|
||||
|
@ -1254,11 +1244,7 @@ nsMsgDBView::GetCellProperties(int32_t aRow, nsTreeColumn* col,
|
|||
}
|
||||
|
||||
const nsAString& colID = col->GetId();
|
||||
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
|
||||
if (colHandler != nullptr) {
|
||||
colHandler->GetCellProperties(aRow, col, properties);
|
||||
} else if (colID[0] == 'c') {
|
||||
// Correspondent.
|
||||
if (colID.First() == 'c' && colID.EqualsLiteral("correspondentCol")) {
|
||||
if (IsOutgoingMsg(msgHdr))
|
||||
properties.AssignLiteral("outgoing");
|
||||
else
|
||||
|
@ -1574,16 +1560,6 @@ nsresult nsMsgDBView::GetDBForViewIndex(nsMsgViewIndex index,
|
|||
NS_IMETHODIMP
|
||||
nsMsgDBView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
|
||||
NS_ENSURE_ARG_POINTER(aCol);
|
||||
// Attempt to retrieve a custom column handler. If it exists call it and
|
||||
// return.
|
||||
const nsAString& colID = aCol->GetId();
|
||||
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
|
||||
|
||||
if (colHandler) {
|
||||
colHandler->GetImageSrc(aRow, aCol, aValue);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -1839,16 +1815,6 @@ nsMsgDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
|
|||
if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
|
||||
|
||||
aValue.Truncate();
|
||||
|
||||
// Attempt to retrieve a custom column handler. If it exists call it and
|
||||
// return.
|
||||
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
|
||||
|
||||
if (colHandler) {
|
||||
colHandler->GetCellText(aRow, aCol, aValue);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
return CellTextForColumn(aRow, colID, aValue);
|
||||
}
|
||||
|
||||
|
@ -1868,6 +1834,14 @@ nsMsgDBView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName,
|
|||
return NS_MSG_INVALID_DBVIEW_INDEX;
|
||||
}
|
||||
|
||||
// Attempt to retrieve a custom column handler. If it exists call it and
|
||||
// return.
|
||||
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(aColumnName);
|
||||
if (colHandler) {
|
||||
colHandler->GetCellText(msgHdr, aValue);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIMsgThread> thread;
|
||||
|
||||
switch (aColumnName.First()) {
|
||||
|
@ -2049,15 +2023,6 @@ nsMsgDBView::CycleCell(int32_t row, nsTreeColumn* col) {
|
|||
|
||||
const nsAString& colID = col->GetId();
|
||||
|
||||
// Attempt to retrieve a custom column handler. If it exists call it and
|
||||
// return.
|
||||
nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
|
||||
|
||||
if (colHandler) {
|
||||
colHandler->CycleCell(row, col);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// The cyclers below don't work for the grouped header dummy row, currently.
|
||||
// A future implementation should consider both collapsed and expanded state.
|
||||
if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort &&
|
||||
|
|
Загрузка…
Ссылка в новой задаче