Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type mismatch handling in merge process #138

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 68 additions & 60 deletions addon/chrome/content/preferences.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,80 +3,88 @@
</linkset>
<vbox id="zotero-prefpane-__addonRef__" onload="Zotero.__addonInstance__.hooks.onPrefsEvent('load', {window})">

<!--<label>-->
<!-- <html:h1>__addonName__</html:h1>-->
<!--</label>-->
<vbox class="pref-section">
<groupbox>
<!-- <label>-->
<!-- <html:h1 data-l10n-id="pref-duplicate-detection-title"></html:h1>-->
<!-- </label>-->
<!--<label>-->
<!-- <html:h1>__addonName__</html:h1>-->
<!--</label>-->
<vbox class="pref-section">
<groupbox>
<!-- <label>-->
<!-- <html:h1 data-l10n-id="pref-duplicate-detection-title"></html:h1>-->
<!-- </label>-->

<label>
<html:h2 data-l10n-id="pref-action-title"></html:h2>
</label>
<label>
<html:h2 data-l10n-id="pref-action-title"></html:h2>
</label>

<label data-l10n-id="pref-default-action-description" />
<!-- `preference="extensions.zotero.__addonRef__.default.action"` also works. -->
<!-- Because __prefsPrefix__ = extensions.zotero.__addonRef__ in package.json-->
<radiogroup
id="zotero-prefpane-__addonRef__-default-action"
preference="__prefsPrefix__.duplicate.default.action"
orient="vertical">
<radio data-l10n-id="pref-default-action-keep-this" value="keep" />
<radio data-l10n-id="pref-default-action-keep-others" value="discard" />
<radio data-l10n-id="pref-default-action-keep-all" value="cancel" />
<radio data-l10n-id="pref-default-action-always-ask" value="ask" />
</radiogroup>
</groupbox>
</vbox>
<label data-l10n-id="pref-default-action-description" />
<!-- `preference="extensions.zotero.__addonRef__.default.action"` also works. -->
<!-- Because __prefsPrefix__ = extensions.zotero.__addonRef__ in package.json-->
<radiogroup id="zotero-prefpane-__addonRef__-default-action"
preference="__prefsPrefix__.duplicate.default.action" orient="vertical">
<radio data-l10n-id="pref-default-action-keep-this" value="keep" />
<radio data-l10n-id="pref-default-action-keep-others" value="discard" />
<radio data-l10n-id="pref-default-action-keep-all" value="cancel" />
<radio data-l10n-id="pref-default-action-always-ask" value="ask" />
</radiogroup>
</groupbox>
</vbox>

<vbox class="pref-section">
<groupbox>
<label>
<html:h2 data-l10n-id="pref-type-mismatch-title"></html:h2>
</label>

<label data-l10n-id="pref-type-mismatch-description" />
<radiogroup id="zotero-prefpane-__addonRef__-type-mismatch"
preference="__prefsPrefix__.duplicate.type.mismatch" orient="vertical">
<radio data-l10n-id="pref-type-mismatch-skip" value="skip" />
<radio data-l10n-id="pref-type-mismatch-convert" value="convert" />
<radio data-l10n-id="pref-type-mismatch-ask" value="ask" />
</radiogroup>
</groupbox>
</vbox>

<vbox class="pref-section">
<groupbox>
<!-- <label>-->
<!-- <html:h1 data-l10n-id="pref-bulk-merge-title"></html:h1>-->
<!-- </label>-->
<label>
<html:h2 data-l10n-id="pref-master-item-title"></html:h2>
</label>
<vbox class="pref-section">
<groupbox>
<!-- <label>-->
<!-- <html:h1 data-l10n-id="pref-bulk-merge-title"></html:h1>-->
<!-- </label>-->
<label>
<html:h2 data-l10n-id="pref-master-item-title"></html:h2>
</label>

<label data-l10n-id="pref-master-item-description" />
<radiogroup
id="zotero-prefpane-__addonRef__-master-item"
preference="__prefsPrefix__.bulk.master.item"
<label data-l10n-id="pref-master-item-description" />
<radiogroup id="zotero-prefpane-__addonRef__-master-item" preference="__prefsPrefix__.bulk.master.item"
orient="vertical">
<radio data-l10n-id="pref-default-master-item-oldest" value="oldest" />
<radio data-l10n-id="pref-default-master-item-newest" value="newest" />
<radio data-l10n-id="pref-default-master-item-modified" value="modified" />
<radio data-l10n-id="pref-default-master-item-detailed" value="detailed" />
<!-- <radio data-l10n-id="pref-default-master-item-always-ask" value="ask" />-->
<radio data-l10n-id="pref-default-master-item-oldest" value="oldest" />
<radio data-l10n-id="pref-default-master-item-newest" value="newest" />
<radio data-l10n-id="pref-default-master-item-modified" value="modified" />
<radio data-l10n-id="pref-default-master-item-detailed" value="detailed" />
<!-- <radio data-l10n-id="pref-default-master-item-always-ask" value="ask" />-->

</radiogroup>
</groupbox>
</vbox>
</radiogroup>
</groupbox>
</vbox>


<vbox class="pref-section">
<groupbox>
<label>
<html:h2 data-l10n-id="pref-view-title"></html:h2>
</label>
<vbox class="pref-section">
<groupbox>
<label>
<html:h2 data-l10n-id="pref-view-title"></html:h2>
</label>

<label data-l10n-id="pref-view-description" />
<checkbox
id="zotero-prefpane-__addonRef__-view-duplicate-stats-enable"
data-l10n-id="pref-view-duplicate-stats"
preference="__prefsPrefix__.duplicate.stats.enable" />
</groupbox>
</vbox>
<label data-l10n-id="pref-view-description" />
<checkbox id="zotero-prefpane-__addonRef__-view-duplicate-stats-enable"
data-l10n-id="pref-view-duplicate-stats" preference="__prefsPrefix__.duplicate.stats.enable" />
</groupbox>
</vbox>

</vbox>


<spacer height="20" flex="1" />
<vbox>
<html:label
data-l10n-id="pref-help"
<html:label data-l10n-id="pref-help"
data-l10n-args='{"time": "__buildTime__","name": "__addonName__","version":"__buildVersion__"}'></html:label>
</vbox>
</vbox>
6 changes: 5 additions & 1 deletion addon/locale/en-US/addon.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ bulk-merge-popup-restore = Restoring: { $item }
duplicate-tooltip = Found { $total } duplicates from { $unique } unique { $items }.
duplicate-not-found-tooltip = No duplicate items found.

type-mismatch-title = Item Types Are Different
type-mismatch-message = Some items have different types. Would you like to convert them to match or skip merging?
type-mismatch-convert = Convert Types
type-mismatch-skip = Skip Merging

## Menus
menuitem-refresh-duplicates = Refresh
Expand All @@ -72,4 +76,4 @@ menuitem-is-duplicate = They are duplicates
# Add non-duplicate
add-not-duplicates-alert-error-duplicates = The selected items are not identified as Duplicates.
add-not-duplicates-alert-error-exist = The selected items already exist.
add-not-duplicates-alert-error-diff-library = Cannot add items from different libraries.
add-not-duplicates-alert-error-diff-library = Cannot add items from different libraries.
12 changes: 11 additions & 1 deletion addon/locale/en-US/preferences.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,19 @@ pref-default-master-item-detailed =
pref-default-master-item-always-ask =
.label = [Always Ask]: Ask for master item every time

pref-type-mismatch-title = Type Mismatch Preferences
pref-type-mismatch-description = How to handle when merging items of different types

pref-type-mismatch-skip =
.label = [Skip]: Skip merging items of different types
pref-type-mismatch-convert =
.label = [Convert]: Convert items to match the master item type
pref-type-mismatch-ask =
.label = [Ask]: Ask what to do when types don't match

pref-view-title = View Preferences
pref-view-description = Choose the widget to display in the main window
pref-view-duplicate-stats =
.label = Append duplicate counts to the "Duplicate Items" entry, e.g., Duplicate Items 2/6

pref-help = { $name } Build { $version } { $time }
pref-help = { $name } Build { $version } { $time }
6 changes: 5 additions & 1 deletion addon/locale/zh-CN/addon.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ bulk-merge-popup-restore = 正在恢复: { $item }
duplicate-tooltip = 检测到 { $total } 个重复项,源自 { $unique } 个唯一条目。
duplicate-not-found-tooltip = 未找到重复条目

type-mismatch-title = 条目类型不匹配
type-mismatch-message = 部分条目类型不一致。您想转换它们的类型还是跳过合并?
type-mismatch-convert = 转换类型
type-mismatch-skip = 跳过合并

## Menus
menuitem-refresh-duplicates = 刷新
Expand All @@ -73,4 +77,4 @@ menuitem-is-duplicate = 标记为重复条目
# Add non-duplicate
add-not-duplicates-alert-error-duplicates = 所选条目没有被识别为重复条目。
add-not-duplicates-alert-error-exist = 所选条目已存在。
add-not-duplicates-alert-error-diff-library = 无法添加不同库中的条目。
add-not-duplicates-alert-error-diff-library = 无法添加不同库中的条目。
12 changes: 11 additions & 1 deletion addon/locale/zh-CN/preferences.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,19 @@ pref-default-master-item-detailed =
pref-default-master-item-always-ask =
.label = [始终询问]: 每次都询问我该如何选择主条目

pref-type-mismatch-title = 条目类型不匹配设置
pref-type-mismatch-description = 如何处理不同类型条目的合并

pref-type-mismatch-skip =
.label = [跳过]: 跳过不同类型条目的合并
pref-type-mismatch-convert =
.label = [转换]: 将条目转换为主条目类型
pref-type-mismatch-ask =
.label = [询问]: 类型不匹配时询问如何处理

pref-view-title = 视图设置
pref-view-description = 选择重复条目的显示方式
pref-view-duplicate-stats =
.label = 在"重复条目"标签后显示重复条目数量。如,重复条目 2/6

pref-help = { $name } Build { $version } { $time }
pref-help = { $name } Build { $version } { $time }
1 change: 1 addition & 0 deletions addon/prefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
pref("__prefsPrefix__.duplicate.default.action", "ask");
pref("__prefsPrefix__.bulk.master.item", "oldest");
pref("__prefsPrefix__.duplicate.stats.enable", true);
pref("__prefsPrefix__.duplicate.type.mismatch", "skip"); // Options: skip, convert, ask
7 changes: 4 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 104 additions & 5 deletions src/modules/merger.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,124 @@
import { getPref, TypeMismatch, setPref } from "../utils/prefs";
import { getString } from "../utils/locale";

async function convertItemType(item: Zotero.Item, targetTypeID: number) {
await Zotero.DB.executeTransaction(async () => {
item.setType(targetTypeID);
await item.save();
// Small delay to ensure DB operations complete
await Zotero.Promise.delay(50);
});
}

export async function merge(
masterItem: Zotero.Item,
otherItems: Zotero.Item[], // Already sorted
): Promise<any> {
Zotero.CollectionTreeCache.clear();

const masterItemType = masterItem.itemTypeID;
otherItems = otherItems.filter((item) => item.itemTypeID === masterItemType);
if (otherItems.length === 0) {
return;
// Check if any items need type conversion
const hasMismatch = otherItems.some(item => item.itemTypeID !== masterItemType);

if (hasMismatch) {
const typeMismatchPref = getPref("duplicate.type.mismatch") as TypeMismatch;

if (typeMismatchPref === TypeMismatch.ASK) {
const dialog = new ztoolkit.Dialog(3, 1)
.setDialogData({
action: TypeMismatch.SKIP,
savePreference: false,
// Add promise to track dialog completion
dialogPromise: Zotero.Promise.defer()
})
.addCell(0, 0, {
tag: "p",
properties: { innerHTML: getString("type-mismatch-message") }
})
.addCell(1, 0, {
tag: "div",
children: [
{
tag: "input",
id: "save_pref",
attributes: {
type: "checkbox",
"data-bind": "savePreference",
"data-prop": "checked"
}
},
{
tag: "label",
attributes: { for: "save_pref" },
properties: { innerHTML: getString("du-dialog-as-default") }
}
]
})
.addButton(getString("type-mismatch-convert"), "btn_convert", {
callback: () => {
dialog.dialogData.action = TypeMismatch.CONVERT;
if (dialog.dialogData.savePreference) {
setPref("duplicate.type.mismatch", TypeMismatch.CONVERT);
}
dialog.dialogData.dialogPromise.resolve();
}
})
.addButton(getString("type-mismatch-skip"), "btn_skip", {
callback: () => {
dialog.dialogData.action = TypeMismatch.SKIP;
if (dialog.dialogData.savePreference) {
setPref("duplicate.type.mismatch", TypeMismatch.SKIP);
}
dialog.dialogData.dialogPromise.resolve();
}
});

dialog.open(getString("type-mismatch-title"), {
centerscreen: true,
resizable: true
});

// Wait for both dialog load and user action
await dialog.dialogData.loadLock?.promise;
await dialog.dialogData.dialogPromise.promise;

if (dialog.dialogData.action === TypeMismatch.CONVERT) {
// Convert items one by one
for (const item of otherItems) {
if (item.itemTypeID !== masterItemType) {
await convertItemType(item, masterItemType);
}
}
} else {
otherItems = otherItems.filter(item => item.itemTypeID === masterItemType);
}
}
else if (typeMismatchPref === TypeMismatch.CONVERT) {
// Convert items one by one
for (const item of otherItems) {
if (item.itemTypeID !== masterItemType) {
await convertItemType(item, masterItemType);
}
}
otherItems = otherItems.filter(item => item.itemTypeID === masterItemType);
}
else { // TypeMismatch.SKIP
otherItems = otherItems.filter(item => item.itemTypeID === masterItemType);
}
}

if (otherItems.length === 0) return;

const masterJSON = masterItem.toJSON();
const candidateJSON: {
//[field in Zotero.Item.DataType]?: string | unknown;
[field in _ZoteroTypes.Item.DataType]?: string | unknown;
} = otherItems.reduce((acc, obj) => ({ ...acc, ...obj.toJSON() }), {});

// Refer to https://github.com/zotero/zotero/blob/main/chrome/content/zotero/duplicatesMerge.js#L151
// New link since 02/02/2024: https://github.com/zotero/zotero/blob/main/chrome/content/zotero/elements/duplicatesMergePane.js#L172
// Exclude certain properties that are empty in the cloned object, so we don't clobber them
const { relations, collections, tags, ...keep } = candidateJSON;
masterItem.fromJSON({ ...keep, ...masterJSON });

return await Zotero.Items.merge(masterItem, otherItems);
}
}
9 changes: 9 additions & 0 deletions src/utils/prefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,12 @@ export enum MasterItem {
MODIFIED = "modified",
DETAILED = "detailed",
}

/**
* NOTE: Corresponding to radio values in addon/chrome/content/preferences.xhtml.
*/
export enum TypeMismatch {
SKIP = "skip",
CONVERT = "convert",
ASK = "ask"
}