commit 9bcdf021dd80b3b762e10d1da0105a19102dbd1a
parent f00e5501e9ec678ef1b92b3a1e217fba359d286a
Author: Dan Stillman <dstillman@zotero.org>
Date: Mon, 16 Jun 2008 05:46:10 +0000
- Fixes tag editing
- Adds tag syncing
- Fixes a few other things
No tag CR yet
Requires new 1.0 DB upgrade
Diffstat:
19 files changed, 1353 insertions(+), 332 deletions(-)
diff --git a/chrome/content/zotero/bindings/tagsbox.xml b/chrome/content/zotero/bindings/tagsbox.xml
@@ -28,16 +28,49 @@
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="tags-box">
<implementation>
- <field name="itemRef"/>
- <property name="item" onget="return this.itemRef;">
+ <field name="clickHandler"/>
+
+ <!-- Modes are predefined settings groups for particular tasks -->
+ <field name="_mode">"view"</field>
+ <property name="mode" onget="return this._mode;">
+ <setter>
+ <![CDATA[
+ this.clickable = false;
+ this.editable = false;
+
+ switch (val) {
+ case 'view':
+ break;
+
+ case 'edit':
+ this.clickable = true;
+ this.editable = true;
+ this.clickHandler = this.showEditor;
+ this.blurHandler = this.hideEditor;
+ break;
+
+ default:
+ throw ("Invalid mode '" + val + "' in tagsbox.xml");
+ }
+
+ this._mode = val;
+ document.getAnonymousNodes(this)[0].setAttribute('mode', val);
+ ]]>
+ </setter>
+ </property>
+
+ <field name="_item"/>
+ <property name="item" onget="return this._item;">
<setter>
<![CDATA[
- this.itemRef = val;
+ this._item = val;
this.reload();
]]>
</setter>
</property>
+
<property name="count"/>
+
<property name="summary">
<getter>
<![CDATA[
@@ -50,7 +83,7 @@
{
for(var i = 0; i < tags.length; i++)
{
- r = r + tags[i].tag + ", ";
+ r = r + tags[i].name + ", ";
}
r = r.substr(0,r.length-2);
}
@@ -60,10 +93,13 @@
]]>
</getter>
</property>
+
+
+
<method name="reload">
<body>
<![CDATA[
- //Zotero.debug('Reloading tags');
+ Zotero.debug('Reloading tags');
var rows = this.id('tagRows');
while(rows.hasChildNodes())
@@ -88,6 +124,8 @@
]]>
</body>
</method>
+
+
<method name="addDynamicRow">
<parameter name="tagObj"/>
<parameter name="tabindex"/>
@@ -95,11 +133,11 @@
<![CDATA[
if (tagObj) {
var tagID = tagObj.id;
- var tag = tagObj.tag;
+ var name = tagObj.name;
var type = tagObj.type;
}
- if (!tag) {
- tag = '';
+ if (!name) {
+ name = '';
}
if (!tabindex)
@@ -128,7 +166,7 @@
// DEBUG: Why won't just this.nextSibling.blur() work?
icon.setAttribute('onclick','if (this.nextSibling.inputField){ this.nextSibling.inputField.blur() }');
- var label = ZoteroItemPane.createValueElement(tag, 'tag', tabindex);
+ var label = this.createValueElement(name, tabindex);
var remove = document.createElement("label");
remove.setAttribute('value','-');
@@ -159,6 +197,284 @@
]]>
</body>
</method>
+
+
+ <method name="createValueElement">
+ <parameter name="valueText"/>
+ <parameter name="tabindex"/>
+ <body>
+ <![CDATA[
+ var valueElement = document.createElement("label");
+ valueElement.setAttribute('fieldname', 'tag');
+ valueElement.setAttribute('flex', 1);
+
+ if (this.clickable) {
+ valueElement.setAttribute('ztabindex', tabindex);
+ valueElement.addEventListener('click', function (event) {
+ /* Skip right-click on Windows */
+ if (event.button) {
+ return;
+ }
+ document.getBindingParent(this).clickHandler(this);
+ }, false);
+ valueElement.className = 'zotero-clicky';
+ }
+
+ this._tabIndexMaxTagsFields = Math.max(this._tabIndexMaxTagsFields, tabindex);
+
+ var firstSpace;
+ if (typeof valueText == 'string') {
+ firstSpace = valueText.indexOf(" ");
+ }
+
+ // 29 == arbitrary length at which to chop uninterrupted text
+ if ((firstSpace == -1 && valueText.length > 29 ) || firstSpace > 29) {
+ valueElement.setAttribute('crop', 'end');
+ valueElement.setAttribute('value',valueText);
+ }
+ else {
+ // Wrap to multiple lines
+ valueElement.appendChild(document.createTextNode(valueText));
+ }
+
+ return valueElement;
+ ]]>
+ </body>
+ </method>
+
+
+ <method name="showEditor">
+ <parameter name="elem"/>
+ <body>
+ <![CDATA[
+ // Blur any active fields
+ /*
+ if (this._dynamicFields) {
+ this._dynamicFields.focus();
+ }
+ */
+
+ Zotero.debug('Showing editor');
+
+ var fieldName = 'tag';
+ var tabindex = elem.getAttribute('ztabindex');
+
+ var tagID = elem.parentNode.getAttribute('id').split('-')[1];
+ var value = tagID ? Zotero.Tags.getName(tagID) : '';
+ var itemID = Zotero.getAncestorByTagName(elem, 'tagsbox').item.id;
+
+ var t = document.createElement("textbox");
+ t.setAttribute('value', value);
+ t.setAttribute('fieldname', fieldName);
+ t.setAttribute('ztabindex', tabindex);
+ t.setAttribute('flex', '1');
+
+ // Add auto-complete
+ t.setAttribute('type', 'autocomplete');
+ t.setAttribute('autocompletesearch', 'zotero');
+ var suffix = itemID ? itemID : '';
+ t.setAttribute('autocompletesearchparam', fieldName + '/' + suffix);
+
+ var box = elem.parentNode;
+ box.replaceChild(t, elem);
+
+ // Prevent error when clicking between a changed field
+ // and another -- there's probably a better way
+ if (!t.select) {
+ return;
+ }
+
+ t.select();
+
+ t.addEventListener('blur', function () {
+ document.getBindingParent(this).blurHandler(this);
+ }, false);
+ t.setAttribute('onkeypress', "return document.getBindingParent(this).handleKeyPress(event)");
+
+ this._tabDirection = false;
+ this._lastTabIndex = tabindex;
+
+ return t;
+ ]]>
+ </body>
+ </method>
+
+
+ <method name="handleKeyPress">
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ var target = event.target;
+ var focused = document.commandDispatcher.focusedElement;
+
+ switch (event.keyCode) {
+ case event.DOM_VK_RETURN:
+ var fieldname = 'tag';
+
+ // Prevent blur on containing textbox
+ // DEBUG: what happens if this isn't present?
+ event.preventDefault();
+
+ // If last tag row, create new one
+ var row = target.parentNode.parentNode;
+ if (row == row.parentNode.lastChild) {
+ this._tabDirection = 1;
+ var lastTag = true;
+ }
+ focused.blur();
+
+ // Return focus to items pane
+ if (!lastTag) {
+ var tree = document.getElementById('zotero-items-tree');
+ if (tree) {
+ tree.focus();
+ }
+ }
+
+ return false;
+
+ case event.DOM_VK_ESCAPE:
+ // Reset field to original value
+ target.value = target.getAttribute('value');
+
+ var tagsbox = Zotero.getAncestorByTagName(focused, 'tagsbox');
+
+ focused.blur();
+
+ if (tagsbox) {
+ tagsbox.closePopup();
+ }
+
+ // Return focus to items pane
+ var tree = document.getElementById('zotero-items-tree');
+ if (tree) {
+ tree.focus();
+ }
+
+ return false;
+
+ case event.DOM_VK_TAB:
+ this._tabDirection = event.shiftKey ? -1 : 1;
+ // Blur the old manually -- not sure why this is necessary,
+ // but it prevents an immediate blur() on the next tag
+ focused.blur();
+ return false;
+ }
+
+ return true;
+ ]]>
+ </body>
+ </method>
+
+
+ <method name="hideEditor">
+ <parameter name="textbox"/>
+ <body>
+ <![CDATA[
+ Zotero.debug('Hiding editor');
+ /*
+ var textbox = Zotero.getAncestorByTagName(t, 'textbox');
+ if (!textbox){
+ Zotero.debug('Textbox not found in hideEditor');
+ return;
+ }
+ */
+
+ // TODO: get rid of this?
+ //var saveChanges = this.saveOnEdit;
+ var saveChanges = true;
+
+ var fieldName = 'tag';
+ var tabindex = textbox.getAttribute('ztabindex');
+
+ //var value = t.value;
+ var value = textbox.value;
+
+ var elem;
+
+ var tagsbox = Zotero.getAncestorByTagName(textbox, 'tagsbox');
+ if (!tagsbox)
+ {
+ Zotero.debug('Tagsbox not found', 1);
+ return;
+ }
+
+ var row = textbox.parentNode;
+ var rows = row.parentNode;
+
+ // Tag id encoded as 'tag-1234'
+ var id = row.getAttribute('id').split('-')[1];
+
+ if (saveChanges) {
+ if (id) {
+ if (value) {
+ // If trying to replace with another existing tag
+ // (which causes a delete of the row),
+ // clear the tab direction so we don't advance
+ // when the notifier kicks in
+ var existing = Zotero.Tags.getID(value, 0);
+ if (existing && id != existing) {
+ this._tabDirection = false;
+ }
+ var changed = tagsbox.replace(id, value);
+ if (changed) {
+ return;
+ }
+ }
+ else {
+ tagsbox.remove(id);
+ return;
+ }
+ }
+ // New tag
+ else {
+ // If this is an existing automatic tag, it's going to be
+ // deleted and the number of rows will stay the same,
+ // so we have to compensate
+ var existingTypes = Zotero.Tags.getTypes(value);
+ if (existingTypes && existingTypes.indexOf(1) != -1) {
+ this._lastTabIndex--;
+ }
+ var id = tagsbox.add(value);
+ }
+ }
+
+ if (id) {
+ elem = this.createValueElement(
+ value,
+ 'tag',
+ tabindex
+ );
+ }
+ else {
+ // Just remove the row
+ //
+ // If there's an open popup, this throws NODE CANNOT BE FOUND
+ try {
+ var row = rows.removeChild(row);
+ }
+ catch (e) {}
+ tagsbox.fixPopup();
+ tagsbox.closePopup();
+
+ this._tabDirection = false;
+ return;
+ }
+
+ var focusMode = 'tags';
+ var focusBox = tagsbox;
+
+ var box = textbox.parentNode;
+ box.replaceChild(elem,textbox);
+
+ if (this._tabDirection) {
+ this._focusNextField(focusBox, this._lastTabIndex, this._tabDirection == -1);
+ }
+ ]]>
+ </body>
+ </method>
+
+
<method name="new">
<body>
<![CDATA[
@@ -167,18 +483,21 @@
]]>
</body>
</method>
+
+
<method name="add">
<parameter name="value"/>
<body>
<![CDATA[
- if (value)
- {
+ if (value) {
return this.item.addTag(value);
}
return false;
]]>
</body>
</method>
+
+
<method name="replace">
<parameter name="oldTagID"/>
<parameter name="newTag"/>
@@ -196,6 +515,8 @@
]]>
</body>
</method>
+
+
<method name="remove">
<parameter name="id"/>
<body>
@@ -204,6 +525,8 @@
]]>
</body>
</method>
+
+
<method name="updateCount">
<parameter name="count"/>
<body>
@@ -235,14 +558,8 @@
]]>
</body>
</method>
- <method name="id">
- <parameter name="id"/>
- <body>
- <![CDATA[
- return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
- ]]>
- </body>
- </method>
+
+
<method name="fixPopup">
<body>
<![CDATA[
@@ -262,6 +579,8 @@
]]>
</body>
</method>
+
+
<method name="closePopup">
<body>
<![CDATA[
@@ -271,10 +590,78 @@
]]>
</body>
</method>
- <method name="getScrollBox">
+
+
+ <!--
+ Advance the field focus forward or backward
+
+ Note: We're basically replicating the built-in tabindex functionality,
+ which doesn't work well with the weird label/textbox stuff we're doing.
+ (The textbox being tabbed away from is deleted before the blur()
+ completes, so it doesn't know where it's supposed to go next.)
+ -->
+ <method name="_focusNextField">
+ <parameter name="box"/>
+ <parameter name="tabindex"/>
+ <parameter name="back"/>
+ <body>
+ <![CDATA[
+ tabindex = parseInt(tabindex);
+ if (back) {
+ switch (tabindex) {
+ case 1:
+ return false;
+
+ default:
+ var nextIndex = tabindex - 1;
+ }
+ }
+ else {
+ switch (tabindex) {
+ case this._tabIndexMaxTagsFields:
+ // In tags box, keep going to create new row
+ var nextIndex = tabindex + 1;
+ break;
+
+ default:
+ var nextIndex = tabindex + 1;
+ }
+ }
+
+ Zotero.debug('Looking for tabindex ' + nextIndex, 4);
+
+ var next = document.getAnonymousNodes(box)[0].
+ getElementsByAttribute('ztabindex', nextIndex);
+ if (!next[0]) {
+ next[0] = box.addDynamicRow();
+ }
+
+ next[0].click();
+ this.ensureElementIsVisible(next[0]);
+ return true;
+ ]]>
+ </body>
+ </method>
+
+
+ <method name="ensureElementIsVisible">
+ <parameter name="elem"/>
+ <body>
+ <![CDATA[
+ var scrollbox = document.getAnonymousNodes(this)[0];
+ var sbo = scrollbox.boxObject;
+ sbo.QueryInterface(Components.interfaces.nsIScrollBoxObject);
+ sbo.ensureElementIsVisible(elem);
+ ]]>
+ </body>
+ </method>
+
+
+ <method name="id">
+ <parameter name="id"/>
<body>
<![CDATA[
- return document.getAnonymousNodes(this)[0];
+ return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0];
]]>
</body>
</method>
diff --git a/chrome/content/zotero/bindings/tagselector.xml b/chrome/content/zotero/bindings/tagselector.xml
@@ -181,7 +181,7 @@
for (var tagID in this._tags) {
// If the last tag was the same, add this tagID and tagType to it
if (tagsToggleBox.lastChild &&
- tagsToggleBox.lastChild.getAttribute('value') == this._tags[tagID].tag) {
+ tagsToggleBox.lastChild.getAttribute('value') == this._tags[tagID].name) {
tagsToggleBox.lastChild.setAttribute('tagID', tagsToggleBox.lastChild.getAttribute('tagID') + '-' + tagID);
tagsToggleBox.lastChild.setAttribute('tagType', tagsToggleBox.lastChild.getAttribute('tagType') + '-' + this._tags[tagID].type);
continue;
@@ -190,7 +190,7 @@
var label = document.createElement('label');
label.setAttribute('onclick', "this.parentNode.parentNode.parentNode.handleTagClick(event, this)");
label.className = 'zotero-clicky';
- label.setAttribute('value', this._tags[tagID].tag);
+ label.setAttribute('value', this._tags[tagID].name);
label.setAttribute('tagID', tagID);
label.setAttribute('tagType', this._tags[tagID].type);
label.setAttribute('context', 'tag-menu');
diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js
@@ -120,16 +120,15 @@ var ZoteroItemPane = new function() {
// Info pane
if (index == 0) {
- var itembox = document.getElementById('zotero-editpane-item-box');
// Hack to allow read-only mode in right pane -- probably a better
// way to allow access to this
if (mode) {
- itembox.mode = mode;
+ _itemBox.mode = mode;
}
else {
- itembox.mode = 'edit';
+ _itemBox.mode = 'edit';
}
- itembox.item = _itemBeingEdited;
+ _itemBox.item = _itemBeingEdited;
}
// Notes pane
@@ -147,7 +146,9 @@ var ZoteroItemPane = new function() {
icon.setAttribute('src','chrome://zotero/skin/treeitem-note.png');
var label = document.createElement('label');
- label.setAttribute('value',_noteToTitle(notes[i].getNote()));
+ var title = Zotero.Notes.noteToTitle(notes[i].getNote());
+ title = title ? title : Zotero.getString('pane.item.notes.untitled');
+ label.setAttribute('value', title);
label.setAttribute('flex','1'); //so that the long names will flex smaller
label.setAttribute('crop','end');
@@ -236,6 +237,13 @@ var ZoteroItemPane = new function() {
// Tags pane
else if(index == 3)
{
+ if (mode) {
+ _tagsBox.mode = mode;
+ }
+ else {
+ _tagsBox.mode = 'edit';
+ }
+
var focusMode = 'tags';
var focusBox = _tagsBox;
_tagsBox.item = _itemBeingEdited;
@@ -269,27 +277,6 @@ var ZoteroItemPane = new function() {
ZoteroPane.openNoteWindow(null, null, _itemBeingEdited.id);
}
- function _noteToTitle(text)
- {
- var MAX_LENGTH = 100;
-
- var t = text.substring(0, MAX_LENGTH);
- var ln = t.indexOf("\n");
- if (ln>-1 && ln<MAX_LENGTH)
- {
- t = t.substring(0, ln);
- }
-
- if(t == "")
- {
- return Zotero.getString('pane.item.notes.untitled');
- }
- else
- {
- return t;
- }
- }
-
function _updateNoteCount()
{
var c = _notesList.childNodes.length;
diff --git a/chrome/content/zotero/itemPane.xul b/chrome/content/zotero/itemPane.xul
@@ -28,45 +28,49 @@
<overlay
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="include.js"/>
<script src="itemPane.js"/>
<deck id="zotero-view-item" flex="1" onselect="if (this.selectedIndex !== '') { ZoteroItemPane.loadPane(this.selectedIndex); }">
<zoteroitembox id="zotero-editpane-item-box" flex="1"/>
- <vbox flex="1">
- <hbox align="center">
- <label id="zotero-editpane-notes-label"/>
- <button label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote();"/>
- </hbox>
- <grid flex="1">
- <columns>
- <column flex="1"/>
- <column/>
- </columns>
- <rows id="zotero-editpane-dynamic-notes" flex="1"/>
- </grid>
- </vbox>
- <vbox flex="1">
- <hbox align="center">
- <label id="zotero-editpane-attachments-label"/>
- <button id="zotero-tb-item-attachments-add" type="menu" label="&zotero.item.add;">
- <menupopup>
- <menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-link" label="&zotero.toolbar.attachment.linked;" oncommand="ZoteroItemPane.addAttachmentFromDialog(true);"/>
- <menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-file" label="&zotero.toolbar.attachment.add;" oncommand="ZoteroItemPane.addAttachmentFromDialog();"/>
- <menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-web-link" label="&zotero.toolbar.attachment.weblink;" oncommand="ZoteroItemPane.addAttachmentFromPage(true);"/>
- <menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-snapshot" label="&zotero.toolbar.attachment.snapshot;" oncommand="ZoteroItemPane.addAttachmentFromPage();"/>
- </menupopup>
- </button>
- </hbox>
- <grid flex="1">
- <columns>
- <column flex="1"/>
- <column/>
- </columns>
- <rows id="zotero-editpane-dynamic-attachments" flex="1"/>
- </grid>
- </vbox>
- <tagsbox id="zotero-editpane-tags" flex="1"/>
- <seealsobox id="zotero-editpane-related" flex="1"/>
+ <vbox flex="1">
+ <hbox align="center">
+ <label id="zotero-editpane-notes-label"/>
+ <button label="&zotero.item.add;" oncommand="ZoteroItemPane.addNote();"/>
+ </hbox>
+ <grid flex="1">
+ <columns>
+ <column flex="1"/>
+ <column/>
+ </columns>
+ <rows id="zotero-editpane-dynamic-notes" flex="1"/>
+ </grid>
+ </vbox>
+
+ <vbox flex="1">
+ <hbox align="center">
+ <label id="zotero-editpane-attachments-label"/>
+ <button id="zotero-tb-item-attachments-add" type="menu" label="&zotero.item.add;">
+ <menupopup>
+ <menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-link" label="&zotero.toolbar.attachment.linked;" oncommand="ZoteroItemPane.addAttachmentFromDialog(true);"/>
+ <menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-file" label="&zotero.toolbar.attachment.add;" oncommand="ZoteroItemPane.addAttachmentFromDialog();"/>
+ <menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-web-link" label="&zotero.toolbar.attachment.weblink;" oncommand="ZoteroItemPane.addAttachmentFromPage(true);"/>
+ <menuitem class="menuitem-iconic" id="zotero-tb-item-attachments-snapshot" label="&zotero.toolbar.attachment.snapshot;" oncommand="ZoteroItemPane.addAttachmentFromPage();"/>
+ </menupopup>
+ </button>
+ </hbox>
+ <grid flex="1">
+ <columns>
+ <column flex="1"/>
+ <column/>
+ </columns>
+ <rows id="zotero-editpane-dynamic-attachments" flex="1"/>
+ </grid>
+ </vbox>
+
+ <tagsbox id="zotero-editpane-tags" flex="1"/>
+
+ <seealsobox id="zotero-editpane-related" flex="1"/>
</deck>
</overlay>
\ No newline at end of file
diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js
@@ -785,8 +785,7 @@ var ZoteroPane = new function()
{
var item = this.itemsView._getItemAtRow(this.itemsView.selection.currentIndex);
- if(item.ref.isNote())
- {
+ if(item.ref.isNote()) {
var noteEditor = document.getElementById('zotero-note-editor');
if (this.itemsView.readOnly) {
noteEditor.mode = 'view';
@@ -817,8 +816,8 @@ var ZoteroPane = new function()
}
document.getElementById('zotero-item-pane-content').selectedIndex = 2;
}
- else if(item.ref.isAttachment())
- {
+
+ else if(item.ref.isAttachment()) {
// DEBUG: this is annoying -- we really want to use an abstracted
// version of createValueElement() from itemPane.js
// (ideally in an XBL binding)
@@ -956,6 +955,8 @@ var ZoteroPane = new function()
document.getElementById('zotero-item-pane-content').selectedIndex = 3;
}
+
+ // Regular item
else
{
ZoteroItemPane.viewItem(item.ref, this.itemsView.readOnly ? 'view' : false);
diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js
@@ -551,11 +551,14 @@ Zotero.Collection.prototype.addItems = function(itemIDs) {
* Remove an item from the collection (does not delete item from library)
**/
Zotero.Collection.prototype.removeItem = function(itemID) {
- var index = this.getChildItems(true).indexOf(itemID);
- if (index == -1) {
- Zotero.debug("Item " + itemID + " not a child of collection "
- + this.id + " in Zotero.Collection.removeItem()");
- return false;
+ var childItems = this.getChildItems(true);
+ if (childItems) {
+ var index = childItems.indexOf(itemID);
+ if (index == -1) {
+ Zotero.debug("Item " + itemID + " not a child of collection "
+ + this.id + " in Zotero.Collection.removeItem()");
+ return false;
+ }
}
Zotero.DB.beginTransaction();
diff --git a/chrome/content/zotero/xpcom/data/creator.js b/chrome/content/zotero/xpcom/data/creator.js
@@ -353,6 +353,7 @@ Zotero.Creator.prototype.erase = function () {
Zotero.debug("Deleting creator " + this.id);
+ // TODO: notifier
var changedItems = [];
var changedItemsNotifierData = {};
diff --git a/chrome/content/zotero/xpcom/data/creators.js b/chrome/content/zotero/xpcom/data/creators.js
@@ -50,14 +50,14 @@ Zotero.Creators = new function() {
return _creatorsByID[creatorID];
}
- var sql = 'SELECT * FROM creators WHERE creatorID=?';
- var result = Zotero.DB.rowQuery(sql, creatorID);
+ var sql = 'SELECT COUNT(*) FROM creators WHERE creatorID=?';
+ var result = Zotero.DB.valueQuery(sql, creatorID);
if (!result) {
return false;
}
- _creatorsByID[creatorID] = new Zotero.Creator(result.creatorID);
+ _creatorsByID[creatorID] = new Zotero.Creator(creatorID);
return _creatorsByID[creatorID];
}
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
@@ -2364,12 +2364,12 @@ Zotero.Item.prototype.getBestSnapshot = function() {
//
// save() is not required for tag functions
//
-Zotero.Item.prototype.addTag = function(tag, type) {
+Zotero.Item.prototype.addTag = function(name, type) {
if (!this.id) {
throw ('Cannot add tag to unsaved item in Item.addTag()');
}
- if (!tag) {
+ if (!name) {
Zotero.debug('Not saving empty tag in Item.addTag()', 2);
return false;
}
@@ -2378,18 +2378,13 @@ Zotero.Item.prototype.addTag = function(tag, type) {
type = 0;
}
- if (type !=0 && type !=1) {
- throw ('Invalid tag type in Item.addTag()');
- }
-
Zotero.DB.beginTransaction();
- var tagID = Zotero.Tags.getID(tag, type);
- var existingTypes = Zotero.Tags.getTypes(tag);
+ var existingTypes = Zotero.Tags.getTypes(name);
if (existingTypes) {
// If existing automatic and adding identical user, remove automatic
if (type == 0 && existingTypes.indexOf(1) != -1) {
- this.removeTag(Zotero.Tags.getID(tag, 1));
+ this.removeTag(Zotero.Tags.getID(name, 1));
}
// If existing user and adding automatic, skip
else if (type == 1 && existingTypes.indexOf(0) != -1) {
@@ -2399,8 +2394,12 @@ Zotero.Item.prototype.addTag = function(tag, type) {
}
}
+ var tagID = Zotero.Tags.getID(name, type);
if (!tagID) {
- var tagID = Zotero.Tags.add(tag, type);
+ var tag = new Zotero.Tag;
+ tag.name = name;
+ tag.type = type;
+ var tagID = tag.save();
}
try {
@@ -2433,38 +2432,20 @@ Zotero.Item.prototype.addTags = function (tags, type) {
Zotero.Item.prototype.addTagByID = function(tagID) {
if (!this.id) {
- throw ('Cannot add tag to unsaved item in Item.addTagByID()');
+ throw ('Cannot add tag to unsaved item in Zotero.Item.addTagByID()');
}
if (!tagID) {
- Zotero.debug('Not saving nonexistent tag in Item.addTagByID()', 2);
- return false;
- }
-
- var sql = "SELECT COUNT(*) FROM tags WHERE tagID = ?";
- var count = !!Zotero.DB.valueQuery(sql, tagID);
-
- if (!count) {
- throw ('Cannot add invalid tag id ' + tagID + ' in Item.addTagByID()');
+ throw ('tagID not provided in Zotero.Item.addTagByID()');
}
- Zotero.DB.beginTransaction();
-
- // If INSERT OR IGNORE gave us affected rows, we wouldn't need this...
- if (this.hasTag(tagID)) {
- Zotero.debug('Item ' + this.id + ' already has tag ' + tagID + ' in Item.addTagByID()');
- Zotero.DB.commitTransaction();
- return false;
+ var tag = Zotero.Tags.get(tagID);
+ if (!tag) {
+ throw ('Cannot add invalid tag ' + tagID + ' in Zotero.Item.addTagByID()');
}
- var sql = "INSERT INTO itemTags VALUES (?,?)";
- Zotero.DB.query(sql, [this.id, tagID]);
-
- Zotero.DB.commitTransaction();
- Zotero.Notifier.trigger('modify', 'item', this.id);
- Zotero.Notifier.trigger('add', 'item-tag', this.id + '-' + tagID);
-
- return true;
+ tag.addItem(this.id);
+ tag.save();
}
Zotero.Item.prototype.hasTag = function(tagID) {
@@ -2484,28 +2465,38 @@ Zotero.Item.prototype.hasTags = function(tagIDs) {
return !!Zotero.DB.valueQuery(sql, [this.id].concat(tagIDs));
}
+/**
+ * Returns all tags assigned to an item
+ *
+ * @return array Array of Zotero.Tag objects
+ */
Zotero.Item.prototype.getTags = function() {
if (!this.id) {
return false;
}
- var sql = "SELECT tagID AS id, tag, tagType AS type FROM tags WHERE tagID IN "
- + "(SELECT tagID FROM itemTags WHERE itemID=" + this.id + ")";
-
- var tags = Zotero.DB.query(sql);
+ var sql = "SELECT tagID, name FROM tags WHERE tagID IN "
+ + "(SELECT tagID FROM itemTags WHERE itemID=?)";
+ var tags = Zotero.DB.query(sql, this.id);
if (!tags) {
return false;
}
var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) {
- return collation.compareString(1, a.tag, b.tag);
+ return collation.compareString(1, a.name, b.name);
});
- return tags;
+
+ var tagObjs = [];
+ for (var i=0; i<tags.length; i++) {
+ var tag = Zotero.Tags.get(tags[i].tagID, true);
+ tagObjs.push(tag);
+ }
+ return tagObjs;
}
Zotero.Item.prototype.getTagIDs = function() {
- var sql = "SELECT tagID FROM itemTags WHERE itemID=" + this.id;
- return Zotero.DB.columnQuery(sql);
+ var sql = "SELECT tagID FROM itemTags WHERE itemID=?";
+ return Zotero.DB.columnQuery(sql, this.id);
}
Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) {
@@ -2537,16 +2528,20 @@ Zotero.Item.prototype.replaceTag = function(oldTagID, newTag) {
Zotero.Item.prototype.removeTag = function(tagID) {
if (!this.id) {
- throw ('Cannot remove tag on unsaved item');
+ throw ('Cannot remove tag on unsaved item in Zotero.Item.removeTag()');
}
- Zotero.DB.beginTransaction();
- var sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=?";
- Zotero.DB.query(sql, [this.id, { int: tagID }]);
- Zotero.Tags.purge();
- Zotero.DB.commitTransaction();
- Zotero.Notifier.trigger('modify', 'item', this.id);
- Zotero.Notifier.trigger('remove', 'item-tag', this.id + '-' + tagID);
+ if (!tagID) {
+ throw ('tagID not provided in Zotero.Item.removeTag()');
+ }
+
+ var tag = Zotero.Tags.get(tagID);
+ if (!tag) {
+ throw ('Cannot remove invalid tag ' + tagID + ' in Zotero.Item.removeTag()');
+ }
+
+ tag.removeItem(this.id);
+ tag.save();
}
Zotero.Item.prototype.removeAllTags = function() {
@@ -3188,10 +3183,14 @@ Zotero.Item.prototype.toArray = function (mode) {
}
}
- arr.tags = this.getTags();
- if (!arr.tags) {
- arr.tags = [];
+ arr.tags = [];
+ var tags = this.getTags();
+ if (tags) {
+ for (var i=0; i<tags.length; i++) {
+ arr.tags.push(tags[i].serialize());
+ }
}
+
arr.related = this.getSeeAlso();
if (!arr.related) {
arr.related = [];
@@ -3312,10 +3311,14 @@ Zotero.Item.prototype.serialize = function(mode) {
}
}
- arr.tags = this.getTags();
- if (!arr.tags) {
- arr.tags = [];
+ arr.tags = [];
+ var tags = this.getTags();
+ if (tags) {
+ for (var i=0; i<tags.length; i++) {
+ arr.tags.push(tags[i].serialize());
+ }
}
+
arr.related = this.getSeeAlso();
if (!arr.related) {
arr.related = [];
diff --git a/chrome/content/zotero/xpcom/data/tag.js b/chrome/content/zotero/xpcom/data/tag.js
@@ -0,0 +1,544 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright (c) 2006 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://chnm.gmu.edu
+
+ Licensed under the Educational Community License, Version 1.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.opensource.org/licenses/ecl1.php
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+
+Zotero.Tag = function(tagID) {
+ this._tagID = tagID ? tagID : null;
+ this._init();
+}
+
+Zotero.Tag.prototype._init = function () {
+ // Public members for access by public methods -- do not access directly
+ this._name = null;
+ this._type = null;
+ this._dateModified = null;
+ this._key = null;
+
+ this._loaded = false;
+ this._changed = false;
+ this._previousData = false;
+
+ this._linkedItemsLoaded = false;
+ this._linkedItems = [];
+}
+
+
+Zotero.Tag.prototype.__defineGetter__('id', function () { return this._tagID; });
+
+Zotero.Tag.prototype.__defineSetter__('tagID', function (val) { this._set('tagID', val); });
+Zotero.Tag.prototype.__defineGetter__('name', function () { return this._get('name'); });
+Zotero.Tag.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
+Zotero.Tag.prototype.__defineGetter__('type', function () { return this._get('type'); });
+Zotero.Tag.prototype.__defineSetter__('type', function (val) { this._set('type', val); });
+Zotero.Tag.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); });
+Zotero.Tag.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); });
+Zotero.Tag.prototype.__defineGetter__('key', function () { return this._get('key'); });
+Zotero.Tag.prototype.__defineSetter__('key', function (val) { this._set('key', val); });
+
+Zotero.Tag.prototype.__defineSetter__('linkedItems', function (arr) { this._setLinkedItems(arr); });
+
+
+Zotero.Tag.prototype._get = function (field) {
+ if (this.id && !this._loaded) {
+ this.load();
+ }
+ return this['_' + field];
+}
+
+
+Zotero.Tag.prototype._set = function (field, val) {
+ switch (field) {
+ case 'id': // set using constructor
+ //case 'tagID': // set using constructor
+ throw ("Invalid field '" + field + "' in Zotero.Tag.set()");
+ }
+
+ if (this.id) {
+ if (!this._loaded) {
+ this.load();
+ }
+ }
+ else {
+ this._loaded = true;
+ }
+
+ if (this['_' + field] != val) {
+ this._prepFieldChange(field);
+
+ switch (field) {
+ default:
+ this['_' + field] = val;
+ }
+ }
+}
+
+
+/**
+ * Check if tag exists in the database
+ *
+ * @return bool TRUE if the tag exists, FALSE if not
+ */
+Zotero.Tag.prototype.exists = function() {
+ if (!this.id) {
+ throw ('tagID not set in Zotero.Tag.exists()');
+ }
+
+ var sql = "SELECT COUNT(*) FROM tags WHERE tagID=?";
+ return !!Zotero.DB.valueQuery(sql, this.id);
+}
+
+
+/*
+ * Build tag from database
+ */
+Zotero.Tag.prototype.load = function() {
+ Zotero.debug("Loading data for tag " + this.id + " in Zotero.Tag.load()");
+
+ if (!this.id) {
+ throw ("tagID not set in Zotero.Tag.load()");
+ }
+
+ var sql = "SELECT name, type, dateModified, key FROM tags WHERE tagID=?";
+ var data = Zotero.DB.rowQuery(sql, this.id);
+
+ this._init();
+ this._loaded = true;
+
+ if (!data) {
+ return;
+ }
+
+ for (var key in data) {
+ this['_' + key] = data[key];
+ }
+}
+
+
+/**
+ * Returns items linked to this tag
+ *
+ * @param bool asIDs Return as itemIDs
+ * @return array Array of Zotero.Item instances or itemIDs,
+ * or FALSE if none
+ */
+Zotero.Tag.prototype.getLinkedItems = function (asIDs) {
+ if (!this._linkedItemsLoaded) {
+ this._loadLinkedItems();
+ }
+
+ if (this._linkedItems.length == 0) {
+ return false;
+ }
+
+ // Return itemIDs
+ if (asIDs) {
+ var ids = [];
+ for each(var item in this._linkedItems) {
+ ids.push(item.id);
+ }
+ return ids;
+ }
+
+ // Return Zotero.Item objects
+ var objs = [];
+ for each(var item in this._linkedItems) {
+ objs.push(item);
+ }
+ return objs;
+}
+
+
+Zotero.Tag.prototype._setLinkedItems = function (itemIDs) {
+ if (!this._linkedItemsLoaded) {
+ this._loadLinkedItems();
+ }
+
+ if (itemIDs.constructor.name != 'Array') {
+ throw ('ids must be an array in Zotero.Tag._setLinkedItems()');
+ }
+
+ var currentIDs = this.getLinkedItems(true);
+ if (!currentIDs) {
+ currentIDs = [];
+ }
+ var oldIDs = []; // children being kept
+ var newIDs = []; // new children
+
+ if (itemIDs.length == 0) {
+ if (currentIDs.length == 0) {
+ Zotero.debug('No linked items added', 4);
+ return false;
+ }
+ }
+ else {
+ for (var i in itemIDs) {
+ var id = parseInt(itemIDs[i]);
+ if (isNaN(id)) {
+ throw ("Invalid itemID '" + itemIDs[i]
+ + "' in Zotero.Tag._setLinkedItems()");
+ }
+
+ if (currentIDs.indexOf(id) != -1) {
+ Zotero.debug("Item " + itemIDs[i]
+ + " is already linked to tag " + this.id);
+ oldIDs.push(id);
+ continue;
+ }
+
+ newIDs.push(id);
+ }
+ }
+
+ // Mark as changed if new or removed ids
+ if (newIDs.length > 0 || oldIDs.length != currentIDs.length) {
+ this._prepFieldChange('linkedItems');
+ }
+ else {
+ Zotero.debug('Linked items not changed in Zotero.Tag._setLinkedItems()', 4);
+ return false;
+ }
+
+ newIDs = oldIDs.concat(newIDs);
+
+ var items = Zotero.Items.get(itemIDs);
+ this._linkedItems = items ? items : [];
+ return true;
+}
+
+
+Zotero.Tag.prototype.addItem = function (itemID) {
+ var current = this.getLinkedItems(true);
+ if (current && current.indexOf(itemID) != -1) {
+ Zotero.debug("Item " + itemID + " already has tag "
+ + this.id + " in Zotero.Tag.addItem()");
+ return false;
+ }
+
+ this._prepFieldChange('linkedItems');
+ var item = Zotero.Items.get(itemID);
+ if (!item) {
+ throw ("Can't link invalid item " + itemID + " to tag " + this.id
+ + " in Zotero.Tag.addItem()");
+ }
+ this._linkedItems.push(item);
+ return true;
+}
+
+
+Zotero.Tag.prototype.removeItem = function (itemID) {
+ var current = this.getLinkedItems(true);
+ if (current) {
+ var index = current.indexOf(itemID);
+ }
+
+ if (!current || index == -1) {
+ Zotero.debug("Item " + itemID + " doesn't have tag "
+ + this.id + " in Zotero.Tag.removeItem()");
+ return false;
+ }
+
+ this._prepFieldChange('linkedItems');
+ this._linkedItems.splice(index, 1);
+ return true;
+}
+
+
+Zotero.Tag.prototype.save = function () {
+ // Default to manual tag
+ if (!this.type) {
+ this.type = 0;
+ }
+
+ if (this.type != 0 && this.type != 1) {
+ throw ('Invalid tag type ' + this.type + ' in Zotero.Tag.save()');
+ }
+
+ if (!this.name) {
+ throw ('Tag name is empty in Zotero.Tag.save()');
+ }
+
+ if (!this._changed) {
+ Zotero.debug("Tag " + this.id + " has not changed");
+ return false;
+ }
+
+
+ Zotero.DB.beginTransaction();
+
+ // ID change
+ if (this._changed.tagID) {
+ var oldID = this._previousData.primary.tagID;
+ var params = [this.id, oldID];
+
+ Zotero.debug("Changing tagID " + oldID + " to " + this.id);
+
+ var row = Zotero.DB.rowQuery("SELECT * FROM tags WHERE tagID=?", oldID);
+
+ // Set type on old row to -1, since there's a UNIQUE on name/type
+ Zotero.DB.query("UPDATE tags SET type=-1 WHERE tagID=?", oldID);
+
+ // Add a new row so we can update the old rows despite FK checks
+ // Use temp key due to UNIQUE constraint on key column
+ Zotero.DB.query("INSERT INTO tags VALUES (?, ?, ?, ?, ?)",
+ [this.id, row.name, row.type, row.dateModified, 'TEMPKEY']);
+
+ Zotero.DB.query("UPDATE itemTags SET tagID=? WHERE tagID=?", params);
+
+ Zotero.DB.query("DELETE FROM tags WHERE tagID=?", oldID);
+
+ Zotero.DB.query("UPDATE tags SET key=? WHERE tagID=?", [row.key, this.id]);
+
+ Zotero.Tags.unload([{ oldID: { name: row.name, type: row.type } }]);
+ Zotero.Notifier.trigger('id-change', 'tag', oldID + '-' + this.id);
+
+ // update caches
+ }
+
+ var isNew = !this.id || !this.exists();
+
+ try {
+ // how to know if date modified changed (in server code too?)
+
+ var tagID = this.id ? this.id : Zotero.ID.get('tags');
+
+ Zotero.debug("Saving tag " + this.id);
+
+ var key = this.key ? this.key : this._generateKey();
+
+ var columns = [
+ 'tagID', 'name', 'type', 'dateModified', 'key'
+ ];
+ var placeholders = ['?', '?', '?', '?', '?'];
+ var sqlValues = [
+ tagID ? { int: tagID } : null,
+ { string: this.name },
+ { int: this.type },
+ // If date modified hasn't changed, use current timestamp
+ this._changed.dateModified ?
+ this.dateModified : Zotero.DB.transactionDateTime,
+ key
+ ];
+
+ var sql = "REPLACE INTO tags (" + columns.join(', ') + ") VALUES ("
+ + placeholders.join(', ') + ")";
+ var insertID = Zotero.DB.query(sql, sqlValues);
+ if (!tagID) {
+ tagID = insertID;
+ }
+
+ // Linked items
+ if (this._changed.linkedItems) {
+ var removed = [];
+ var newids = [];
+ var currentIDs = this.getLinkedItems(true);
+ if (!currentIDs) {
+ currentIDs = [];
+ }
+
+ if (this._previousData.linkedItems) {
+ for each(var id in this._previousData.linkedItems) {
+ if (currentIDs.indexOf(id) == -1) {
+ removed.push(id);
+ }
+ }
+ }
+ for each(var id in currentIDs) {
+ if (this._previousData.linkedItems &&
+ this._previousData.linkedItems.indexOf(id) != -1) {
+ continue;
+ }
+ newids.push(id);
+ }
+
+ if (removed.length) {
+ var sql = "DELETE FROM itemTags WHERE tagID=? "
+ + "AND itemID IN ("
+ + removed.map(function () '?').join()
+ + ")";
+ Zotero.DB.query(sql, [tagID].concat(removed));
+ }
+
+ if (newids.length) {
+ var sql = "INSERT INTO itemTags (itemID, tagID) VALUES (?,?)";
+ var insertStatement = Zotero.DB.getStatement(sql);
+
+ for each(var itemID in newids) {
+ insertStatement.bindInt32Parameter(0, itemID);
+ insertStatement.bindInt32Parameter(1, tagID);
+
+ try {
+ insertStatement.execute();
+ }
+ catch (e) {
+ throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
+ }
+ }
+ }
+
+ //Zotero.Notifier.trigger('add', 'tag-item', this.id + '-' + itemID);
+
+ // TODO: notify linked items of name changes?
+ // Zotero.Notifier.trigger('modify', 'item', itemIDs);
+ }
+
+ Zotero.DB.commitTransaction();
+ }
+ catch (e) {
+ Zotero.DB.rollbackTransaction();
+ throw (e);
+ }
+
+ // If successful, set values in object
+ if (!this.id) {
+ this._tagID = tagID;
+ }
+
+ if (!this.key) {
+ this._key = key;
+ }
+
+ Zotero.Tags.reload(this.id);
+
+ if (isNew) {
+ Zotero.Notifier.trigger('add', 'tag', this.id);
+ }
+ else {
+ Zotero.Notifier.trigger('modify', 'tag', this.id, this._previousData);
+ }
+
+ return this.id;
+}
+
+
+Zotero.Tag.prototype.serialize = function () {
+ var obj = {
+ primary: {
+ tagID: this.id,
+ dateModified: this.dateModified,
+ key: this.key
+ },
+ name: this.name,
+ type: this.type,
+ linkedItems: this.getLinkedItems(true),
+ };
+ return obj;
+}
+
+
+
+/**
+ * Remove tag from all linked items
+ *
+ * Tags.erase() should be used externally instead of this
+ *
+ * Actual deletion of tag occurs in Zotero.Tags.purge(),
+ * which is called by Tags.erase()
+ */
+Zotero.Tag.prototype.erase = function () {
+ Zotero.debug('Deleting tag ' + this.id);
+
+ if (!this.id) {
+ return;
+ }
+
+ var linkedItems = [];
+ var linkedItemsNotifierData = {};
+
+ Zotero.DB.beginTransaction();
+
+ var deletedTagNotifierData = {};
+ deletedTagNotifierData[this.id] = { old: this.serialize() };
+
+ var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
+ var linkedItemIDs = Zotero.DB.columnQuery(sql, this.id);
+
+ if (!linkedItemIDs) {
+ Zotero.DB.commitTransaction();
+ return;
+ }
+
+ var sql = "DELETE FROM itemTags WHERE tagID=?";
+ Zotero.DB.query(sql, this.id);
+
+ var itemTags = [];
+ for each(var itemID in linkedItemIDs) {
+ var item = Zotero.Items.get(itemID)
+ if (!item) {
+ throw ('Linked item not found in Zotero.Tag.erase()');
+ }
+ linkedItems.push(itemID);
+ linkedItemsNotifierData[itemID] = { old: item.serialize() };
+
+ itemTags.push(itemID + '-' + this.id);
+ }
+ Zotero.Notifier.trigger('remove', 'item-tag', itemTags);
+
+ // Send notification of linked items
+ if (linkedItems.length) {
+ Zotero.Notifier.trigger('modify', 'item', linkedItems, linkedItemsNotifierData);
+ }
+
+ Zotero.Notifier.trigger('delete', 'tag', this.id, deletedTagNotifierData);
+
+ Zotero.DB.commitTransaction();
+ return;
+}
+
+
+Zotero.Tag.prototype._loadLinkedItems = function() {
+ if (!this._loaded) {
+ this.load();
+ }
+
+ var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
+ var ids = Zotero.DB.columnQuery(sql, this.id);
+
+ this._linkedItems = [];
+
+ if (ids) {
+ for each(var id in ids) {
+ this._linkedItems.push(Zotero.Items.get(id));
+ }
+ }
+
+ this._linkedItemsLoaded = true;
+}
+
+
+Zotero.Tag.prototype._prepFieldChange = function (field) {
+ if (!this._changed) {
+ this._changed = {};
+ }
+ this._changed[field] = true;
+
+ // Save a copy of the data before changing
+ // TODO: only save previous data if tag exists
+ if (this.id && this.exists() && !this._previousData) {
+ this._previousData = this.serialize();
+ }
+}
+
+
+Zotero.Tag.prototype._generateKey = function () {
+ return Zotero.ID.getKey();
+}
+
diff --git a/chrome/content/zotero/xpcom/data/tags.js b/chrome/content/zotero/xpcom/data/tags.js
@@ -33,37 +33,37 @@ Zotero.Tags = new function() {
this.getID = getID;
this.getIDs = getIDs;
this.getTypes = getTypes;
+ this.getUpdated = getUpdated;
this.getAll = getAll;
this.getAllWithinSearch = getAllWithinSearch;
this.getTagItems = getTagItems;
this.search = search;
- this.add = add;
this.rename = rename;
- this.remove = remove;
+ this.reload = reload;
+ this.erase = erase;
this.purge = purge;
- this.toArray = toArray;
+ this.unload = unload;
/*
* Returns a tag and type for a given tagID
*/
- function get(tagID) {
+ function get(tagID, skipCheck) {
if (_tagsByID[tagID]) {
return _tagsByID[tagID];
}
- var sql = 'SELECT tag, tagType FROM tags WHERE tagID=?';
- var result = Zotero.DB.rowQuery(sql, tagID);
-
- if (!result) {
- return false;
+ if (!skipCheck) {
+ var sql = 'SELECT COUNT(*) FROM tags WHERE tagID=?';
+ var result = Zotero.DB.valueQuery(sql, tagID);
+
+ if (!result) {
+ return false;
+ }
}
- _tagsByID[tagID] = {
- tag: result.tag,
- type: result.tagType
- };
- return result;
+ _tagsByID[tagID] = new Zotero.Tag(tagID);
+ return _tagsByID[tagID];
}
@@ -72,31 +72,31 @@ Zotero.Tags = new function() {
*/
function getName(tagID) {
if (_tagsByID[tagID]) {
- return _tagsByID[tagID].tag;
+ return _tagsByID[tagID].name;
}
var tag = this.get(tagID);
- return _tagsByID[tagID] ? _tagsByID[tagID].tag : false;
+ return _tagsByID[tagID] ? _tagsByID[tagID].name : false;
}
/*
* Returns the tagID matching given tag and type
*/
- function getID(tag, type) {
- if (_tags[type] && _tags[type]['_' + tag]) {
- return _tags[type]['_' + tag];
+ function getID(name, type) {
+ if (_tags[type] && _tags[type]['_' + name]) {
+ return _tags[type]['_' + name];
}
- var sql = 'SELECT tagID FROM tags WHERE tag=? AND tagType=?';
- var tagID = Zotero.DB.valueQuery(sql, [tag, type]);
+ var sql = 'SELECT tagID FROM tags WHERE name=? AND type=?';
+ var tagID = Zotero.DB.valueQuery(sql, [name, type]);
if (tagID) {
if (!_tags[type]) {
_tags[type] = [];
}
- _tags[type]['_' + tag] = tagID;
+ _tags[type]['_' + name] = tagID;
}
return tagID;
@@ -106,30 +106,40 @@ Zotero.Tags = new function() {
/*
* Returns all tagIDs for this tag (of all types)
*/
- function getIDs(tag) {
- var sql = 'SELECT tagID FROM tags WHERE tag=?';
- return Zotero.DB.columnQuery(sql, [tag]);
+ function getIDs(name) {
+ var sql = 'SELECT tagID FROM tags WHERE name=?';
+ return Zotero.DB.columnQuery(sql, [name]);
}
/*
- * Returns an array of tagTypes for tags matching given tag
+ * Returns an array of tag types for tags matching given tag
*/
- function getTypes(tag) {
- var sql = 'SELECT tagType FROM tags WHERE tag=?';
- return Zotero.DB.columnQuery(sql, [tag]);
+ function getTypes(name) {
+ var sql = 'SELECT type FROM tags WHERE name=?';
+ return Zotero.DB.columnQuery(sql, [name]);
+ }
+
+
+ function getUpdated(date) {
+ var sql = "SELECT tagID FROM tags";
+ if (date) {
+ sql += " WHERE dateModified>?";
+ return Zotero.DB.columnQuery(sql, Zotero.Date.dateToSQL(date, true));
+ }
+ return Zotero.DB.columnQuery(sql);
}
/**
* Get all tags indexed by tagID
*
- * _types_ is an optional array of tagTypes to fetch
+ * _types_ is an optional array of tag types to fetch
*/
function getAll(types) {
- var sql = "SELECT tagID, tag, tagType FROM tags ";
+ var sql = "SELECT tagID, name FROM tags ";
if (types) {
- sql += "WHERE tagType IN (" + types.join() + ") ";
+ sql += "WHERE type IN (" + types.join() + ") ";
}
var tags = Zotero.DB.query(sql);
if (!tags) {
@@ -138,15 +148,13 @@ Zotero.Tags = new function() {
var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) {
- return collation.compareString(1, a.tag, b.tag);
+ return collation.compareString(1, a.name, b.name);
});
var indexed = {};
for (var i=0; i<tags.length; i++) {
- indexed[tags[i].tagID] = {
- tag: tags[i].tag,
- type: tags[i].tagType
- };
+ var tag = this.get(tags[i].tagID, true);
+ indexed[tags[i].tagID] = tag;
}
return indexed;
}
@@ -155,7 +163,7 @@ Zotero.Tags = new function() {
/*
* Get all tags within the items of a Zotero.Search object
*
- * _types_ is an optional array of tagTypes to fetch
+ * _types_ is an optional array of tag types to fetch
*/
function getAllWithinSearch(search, types) {
// Save search results to temporary table
@@ -176,11 +184,11 @@ Zotero.Tags = new function() {
return {};
}
- var sql = "SELECT DISTINCT tagID, tag, tagType FROM itemTags "
+ var sql = "SELECT DISTINCT tagID, name, type FROM itemTags "
+ "NATURAL JOIN tags WHERE itemID IN "
+ "(SELECT itemID FROM " + tmpTable + ") ";
if (types) {
- sql += "AND tagType IN (" + types.join() + ") ";
+ sql += "AND type IN (" + types.join() + ") ";
}
var tags = Zotero.DB.query(sql);
@@ -192,15 +200,13 @@ Zotero.Tags = new function() {
var collation = Zotero.getLocaleCollation();
tags.sort(function(a, b) {
- return collation.compareString(1, a.tag, b.tag);
+ return collation.compareString(1, a.name, b.name);
});
var indexed = {};
for (var i=0; i<tags.length; i++) {
- indexed[tags[i].tagID] = {
- tag: tags[i].tag,
- type: tags[i].tagType
- };
+ var tag = this.get(tags[i].tagID, true);
+ indexed[tags[i].tagID] = tag;
}
return indexed;
}
@@ -213,76 +219,49 @@ Zotero.Tags = new function() {
function search(str) {
- var sql = 'SELECT tagID, tag, tagType FROM tags';
+ var sql = 'SELECT tagID, name, type FROM tags';
if (str) {
- sql += ' WHERE tag LIKE ?';
+ sql += ' WHERE name LIKE ?';
}
- sql += ' ORDER BY tag COLLATE NOCASE';
var tags = Zotero.DB.query(sql, str ? '%' + str + '%' : undefined);
- var indexed = {};
- for each(var tag in tags) {
- indexed[tag.tagID] = {
- tag: tag.tag,
- type: tag.tagType
- };
- }
- return indexed;
- }
-
-
- /*
- * Add a new tag to the database
- *
- * Returns new tagID
- */
- function add(tag, type) {
- if (type != 0 && type != 1) {
- throw ('Invalid tag type ' + type + ' in Tags.add()');
- }
- if (!type) {
- type = 0;
+ if (!tags) {
+ return {};
}
- Zotero.debug('Adding new tag of type ' + type, 4);
-
- Zotero.DB.beginTransaction();
-
- var sql = 'INSERT INTO tags VALUES (?,?,?)';
- var rnd = Zotero.ID.get('tags');
- Zotero.DB.query(sql, [{int: rnd}, {string: tag}, {int: type}]);
+ var collation = Zotero.getLocaleCollation();
+ tags.sort(function(a, b) {
+ return collation.compareString(1, a.name, b.name);
+ });
- Zotero.DB.commitTransaction();
- Zotero.Notifier.trigger('add', 'tag', rnd);
- return rnd;
+ var indexed = {};
+ for (var i=0; i<tags.length; i++) {
+ var tag = this.get(tags[i].tagID, true);
+ indexed[tags[i].tagID] = tag;
+ }
+ return indexed;
}
- function rename(tagID, tag) {
+ function rename(tagID, name) {
Zotero.debug('Renaming tag', 4);
Zotero.DB.beginTransaction();
var tagObj = this.get(tagID);
- var oldName = tagObj.tag;
+ var oldName = tagObj.name;
var oldType = tagObj.type;
var notifierData = {};
- notifierData[this.id] = { old: this.toArray() };
+ notifierData[tagID] = { old: tag.serialize() };
- if (oldName == tag) {
- // Convert unchanged automatic tags to manual
- if (oldType != 0) {
- var sql = "UPDATE tags SET tagType=0 WHERE tagID=?";
- Zotero.DB.query(sql, tagID);
- Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData);
- }
+ if (oldName == name) {
Zotero.DB.commitTransaction();
return;
}
// Check if the new tag already exists
- var sql = "SELECT tagID FROM tags WHERE tag=? AND tagType=0";
- var existingTagID = Zotero.DB.valueQuery(sql, tag);
+ var sql = "SELECT tagID FROM tags WHERE name=? AND type=0";
+ var existingTagID = Zotero.DB.valueQuery(sql, name);
if (existingTagID) {
var itemIDs = this.getTagItems(tagID);
var existingItemIDs = this.getTagItems(existingTagID);
@@ -316,54 +295,44 @@ Zotero.Tags = new function() {
}
}
Zotero.Notifier.trigger('add', 'item-tag', itemTags);
+ // TODO: notify linked items?
+ //Zotero.Notifier.trigger('modify', 'item', itemIDs);
- Zotero.Notifier.trigger('modify', 'item', itemIDs);
Zotero.DB.commitTransaction();
return;
}
- // 0 == user tag -- we set all renamed tags to 0
- var sql = "UPDATE tags SET tag=?, tagType=0 WHERE tagID=?";
- Zotero.DB.query(sql, [{string: tag}, tagID]);
-
- var itemIDs = this.getTagItems(tagID);
-
- if (_tags[oldType]) {
- delete _tags[oldType]['_' + oldName];
- }
- delete _tagsByID[tagID];
+ tagObj.name = name;
+ // Set all renamed tags to manual
+ tagObj.type = 0;
+ tagObj.save();
Zotero.DB.commitTransaction();
-
- Zotero.Notifier.trigger('modify', 'item', itemIDs);
- Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData);
}
- function remove(tagID) {
- Zotero.DB.beginTransaction();
+ function reload(ids) {
+ this.unload(ids);
+ }
+
+
+ function erase(ids) {
+ ids = Zotero.flattenArguments(ids);
- var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
- var itemIDs = Zotero.DB.columnQuery(sql, tagID);
+ var erasedTags = {};
- if (!itemIDs) {
- Zotero.DB.commitTransaction();
- return;
+ Zotero.DB.beginTransaction();
+ for each(var id in ids) {
+ var tag = this.get(id);
+ if (tag) {
+ erasedTags[id] = tag.serialize();
+ tag.erase();
+ }
}
- var sql = "DELETE FROM itemTags WHERE tagID=?";
- Zotero.DB.query(sql, tagID);
-
- Zotero.Notifier.trigger('modify', 'item', itemIDs)
- var itemTags = [];
- for (var i in itemIDs) {
- itemTags.push(itemIDs[i] + '-' + tagID);
- }
- Zotero.Notifier.trigger('remove', 'item-tag', itemTags);
+ this.unload(ids);
- this.purge();
Zotero.DB.commitTransaction();
- return;
}
@@ -373,47 +342,72 @@ Zotero.Tags = new function() {
* Returns removed tagIDs on success
*/
function purge() {
- Zotero.DB.beginTransaction();
-
- var sql = 'SELECT tagID, tag, tagType FROM tags WHERE tagID '
- + 'NOT IN (SELECT tagID FROM itemTags);';
- var toDelete = Zotero.DB.query(sql);
-
- if (!toDelete) {
- Zotero.DB.commitTransaction();
- return false;
- }
-
- var purged = [];
- var notifierData = {};
-
- // Clear tag entries in internal array
- for each(var tag in toDelete) {
- notifierData[tag.tagID] = { old: Zotero.Tags.toArray(tag.tagID) }
+ Zotero.UnresponsiveScriptIndicator.disable();
+ try {
+ Zotero.DB.beginTransaction();
+
+ var sql = "CREATE TEMPORARY TABLE tagDelete AS "
+ + "SELECT tagID FROM tags WHERE tagID "
+ + "NOT IN (SELECT tagID FROM itemTags);";
+ Zotero.DB.query(sql);
+
+ sql = "CREATE INDEX tagDelete_tagID ON tagDelete(tagID)";
+ Zotero.DB.query(sql);
+
+ sql = "SELECT * FROM tagDelete";
+ var toDelete = Zotero.DB.columnQuery(sql);
+
+ if (!toDelete) {
+ Zotero.DB.rollbackTransaction();
+ return;
+ }
- purged.push(tag.tagID);
- if (_tags[tag.tagType]) {
- delete _tags[tag.tagType]['_' + tag.tag];
+ var notifierData = {};
+
+ for each(var tagID in toDelete) {
+ var tag = Zotero.Tags.get(tagID);
+ Zotero.debug(tag);
+ notifierData[tagID] = { old: tag.serialize() }
}
- delete _tagsByID[tag.tagID];
+
+ this.unload(toDelete);
+
+ sql = "DELETE FROM tags WHERE tagID IN "
+ + "(SELECT tagID FROM tagDelete);";
+ Zotero.DB.query(sql);
+
+ sql = "DROP TABLE tagDelete";
+ Zotero.DB.query(sql);
+
+ Zotero.DB.commitTransaction();
+
+ Zotero.Notifier.trigger('delete', 'tag', toDelete, notifierData);
+ }
+ catch (e) {
+ Zotero.DB.rollbackTransaction();
+ throw (e);
+ }
+ finally {
+ Zotero.UnresponsiveScriptIndicator.enable();
}
-
- sql = 'DELETE FROM tags WHERE tagID NOT IN '
- + '(SELECT tagID FROM itemTags);';
- var result = Zotero.DB.query(sql);
-
- Zotero.DB.commitTransaction();
-
- Zotero.Notifier.trigger('delete', 'tag', purged, notifierData);
-
- return toDelete;
}
- function toArray(tagID) {
- var obj = this.get(tagID);
- obj.id = tagID;
- return obj;
+ /**
+ * Unload tags from caches
+ *
+ * @param int|array ids One or more tagIDs
+ */
+ function unload() {
+ var ids = Zotero.flattenArguments(arguments);
+
+ for each(var id in ids) {
+ var tag = _tagsByID[id];
+ delete _tagsByID[id];
+ if (tag && _tags[tag.type]) {
+ delete _tags[tag.type]['_' + tag.name];
+ }
+ }
}
}
diff --git a/chrome/content/zotero/xpcom/id.js b/chrome/content/zotero/xpcom/id.js
@@ -47,6 +47,7 @@ Zotero.ID = new function () {
case 'creatorData':
case 'collections':
case 'savedSearches':
+ case 'tags':
var id = _getNextAvailable(table, skip);
if (!id && notNull) {
return _getNext(table, skip);
@@ -57,7 +58,6 @@ Zotero.ID = new function () {
//
// TODO: use autoincrement instead where available in 1.5
case 'itemDataValues':
- case 'tags':
var id = _getNextAvailable(table, skip);
if (!id) {
// If we can't find an empty id quickly, just use MAX() + 1
diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js
@@ -1447,6 +1447,27 @@ Zotero.Schema = new function(){
}
}
statement.reset();
+
+ // Tags
+ var tags = Zotero.DB.query("SELECT * FROM tags");
+ Zotero.DB.query("DROP TABLE tags");
+ Zotero.DB.query("CREATE TABLE tags (\n tagID INTEGER PRIMARY KEY,\n name TEXT,\n type INT,\n dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,\n key TEXT NOT NULL UNIQUE,\n UNIQUE (name, type)\n)");
+ var statement = Zotero.DB.getStatement("INSERT INTO tags (tagID, name, type, key) VALUES (?,?,?,?)");
+ for (var j=0, len=searches.length; j<len; j++) {
+ statement.bindInt32Parameter(0, tags[j].tagID);
+ statement.bindUTF8StringParameter(1, tags[j].tag);
+ statement.bindInt32Parameter(2, tags[j].tagType);
+ var key = Zotero.ID.getKey();
+ statement.bindStringParameter(3, key);
+
+ try {
+ statement.execute();
+ }
+ catch (e) {
+ throw (Zotero.DB.getLastErrorString());
+ }
+ }
+ statement.reset();
}
}
diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js
@@ -1765,7 +1765,7 @@ Zotero.SearchConditions = new function(){
doesNotContain: true
},
table: 'itemTags',
- field: 'tag'
+ field: 'name'
},
{
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
@@ -27,7 +27,12 @@ Zotero.Sync = new function() {
search: {
singular: 'Search',
plural: 'Searches'
+ },
+ tag: {
+ singular: 'Tag',
+ plural: 'Tags'
}
+
};
});
@@ -1068,6 +1073,8 @@ Zotero.Sync.Server.Data = new function() {
this.xmlToCreator = xmlToCreator;
this.searchToXML = searchToXML;
this.xmlToSearch = xmlToSearch;
+ this.tagToXML = tagToXML;
+ this.xmlToTag = xmlToTag;
var _noMergeTypes = ['search'];
@@ -1208,7 +1215,7 @@ Zotero.Sync.Server.Data = new function() {
// Update id in local updates array
var index = uploadIDs.updated[types].indexOf(oldID);
if (index == -1) {
- _error("Local " + type + " " + oldID + " not in "
+ throw ("Local " + type + " " + oldID + " not in "
+ "update array when changing id");
}
uploadIDs.updated[types][index] = newID;
@@ -1256,7 +1263,7 @@ Zotero.Sync.Server.Data = new function() {
if (type != 'item') {
alert('Delete reconciliation unimplemented for ' + types);
- _error('Delete reconciliation unimplemented for ' + types);
+ throw ('Delete reconciliation unimplemented for ' + types);
}
var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode);
@@ -1653,7 +1660,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
else if (skipPrimary) {
- _error("Cannot use skipPrimary with existing item in "
+ throw ("Cannot use skipPrimary with existing item in "
+ "Zotero.Sync.Server.Data.xmlToItem()");
}
@@ -1699,7 +1706,7 @@ Zotero.Sync.Server.Data = new function() {
for each(var creator in xmlItem.creator) {
var pos = parseInt(creator.@index);
if (pos != i) {
- _error('No creator in position ' + i);
+ throw ('No creator in position ' + i);
}
item.setCreator(
@@ -1799,7 +1806,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
else if (skipPrimary) {
- _error("Cannot use skipPrimary with existing collection in "
+ throw ("Cannot use skipPrimary with existing collection in "
+ "Zotero.Sync.Server.Data.xmlToCollection()");
}
@@ -1877,7 +1884,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
else if (skipPrimary) {
- _error("Cannot use skipPrimary with existing creator in "
+ throw ("Cannot use skipPrimary with existing creator in "
+ "Zotero.Sync.Server.Data.xmlToCreator()");
}
@@ -1960,7 +1967,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
else if (skipPrimary) {
- _error("Cannot use new id with existing search in "
+ throw ("Cannot use new id with existing search in "
+ "Zotero.Sync.Server.Data.xmlToSearch()");
}
@@ -2010,4 +2017,63 @@ Zotero.Sync.Server.Data = new function() {
return search;
}
+
+
+ function tagToXML(tag) {
+ var xml = <tag/>;
+
+ xml.@id = tag.id;
+ xml.@name = tag.name;
+ if (tag.type) {
+ xml.@type = tag.type;
+ }
+ xml.@dateModified = tag.dateModified;
+ xml.@key = tag.key;
+ var linkedItems = tag.getLinkedItems(true);
+ if (linkedItems) {
+ xml.items = linkedItems.join(' ');
+ }
+ return xml;
+ }
+
+
+ /**
+ * Convert E4X <tag> object into an unsaved Zotero.Tag
+ *
+ * @param object xmlTag E4X XML node with tag data
+ * @param object tag (Optional) Existing Zotero.Tag to update
+ * @param bool skipPrimary (Optional) Ignore passed primary fields
+ */
+ function xmlToTag(xmlTag, tag, skipPrimary) {
+ if (!tag) {
+ if (skipPrimary) {
+ tag = new Zotero.Tag;
+ }
+ else {
+ tag = new Zotero.Tag(parseInt(xmlTag.@id));
+ /*
+ if (tag.exists()) {
+ throw ("Tag specified in XML node already exists "
+ + "in Zotero.Sync.Server.Data.xmlToTag()");
+ }
+ */
+ }
+ }
+ else if (skipPrimary) {
+ throw ("Cannot use new id with existing tag in "
+ + "Zotero.Sync.Server.Data.xmlToTag()");
+ }
+
+ tag.name = xmlTag.@name.toString();
+ tag.type = parseInt(xmlTag.@type);
+ if (!skipPrimary) {
+ tag.dateModified = xmlTag.@dateModified.toString();
+ tag.key = xmlTag.@key.toString();
+ }
+
+ var str = xmlTag.items ? xmlTag.items.toString() : false;
+ tag.linkedItems = str ? str.split(' ') : [];
+
+ return tag;
+ }
}
diff --git a/components/zotero-autocomplete.js b/components/zotero-autocomplete.js
@@ -134,15 +134,21 @@ ZoteroAutoComplete.prototype.startSearch = function(searchString, searchParam,
break;
case 'tag':
- var sql = "SELECT tag FROM tags WHERE tag LIKE ?";
+ var sql = "SELECT name FROM tags WHERE name LIKE ?";
var sqlParams = [searchString + '%'];
if (extra){
sql += " AND tagID NOT IN (SELECT tagID FROM itemTags WHERE "
+ "itemID = ?)";
sqlParams.push(extra);
}
- sql += " ORDER BY tag";
var results = this._zotero.DB.columnQuery(sql, sqlParams);
+ if (results) {
+ var collation = Zotero.getLocaleCollation();
+ results.sort(function(a, b) {
+ return collation.compareString(1, a, b);
+ });
+ }
+
break;
case 'creator':
diff --git a/components/zotero-service.js b/components/zotero-service.js
@@ -14,13 +14,14 @@ var ZoteroWrapped = this;
* Include the core objects to be stored within XPCOM
*********************************************************************/
-var xpcomFiles = [ 'zotero',
+var xpcomFiles = ['zotero',
'annotate', 'attachments', 'cite', 'cite_compat', 'collectionTreeView',
- 'dataServer', 'data_access', 'data/item', 'data/items', 'data/collection', 'data/collections',
- 'data/cachedTypes', 'data/creator', 'data/creators', 'data/itemFields',
- 'data/notes', 'data/tags', 'db', 'file', 'fulltext', 'id', 'ingester', 'integration',
- 'itemTreeView', 'mime', 'notifier', 'progressWindow', 'quickCopy', 'report',
- 'schema', 'search', 'sync', 'timeline', 'translate', 'utilities', 'zeroconf'];
+ 'dataServer', 'data_access', 'data/item', 'data/items', 'data/collection',
+ 'data/collections', 'data/cachedTypes', 'data/creator', 'data/creators',
+ 'data/itemFields', 'data/notes', 'data/tag', 'data/tags', 'db', 'file',
+ 'fulltext', 'id', 'ingester', 'integration', 'itemTreeView', 'mime',
+ 'notifier', 'progressWindow', 'quickCopy', 'report', 'schema', 'search',
+ 'sync', 'timeline', 'translate', 'utilities', 'zeroconf'];
for (var i=0; i<xpcomFiles.length; i++) {
Cc["@mozilla.org/moz/jssubscript-loader;1"]
diff --git a/system.sql b/system.sql
@@ -1249,3 +1249,4 @@ INSERT INTO "syncObjectTypes" VALUES(1, 'collection');
INSERT INTO "syncObjectTypes" VALUES(2, 'creator');
INSERT INTO "syncObjectTypes" VALUES(3, 'item');
INSERT INTO "syncObjectTypes" VALUES(4, 'search');
+INSERT INTO "syncObjectTypes" VALUES(5, 'tag');
diff --git a/userdata.sql b/userdata.sql
@@ -71,9 +71,11 @@ CREATE INDEX itemAttachments_mimeType ON itemAttachments(mimeType);
-- Individual entries for each tag
CREATE TABLE tags (
tagID INTEGER PRIMARY KEY,
- tag TEXT,
- tagType INT,
- UNIQUE (tag, tagType)
+ name TEXT,
+ type INT,
+ dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ key TEXT NOT NULL UNIQUE,
+ UNIQUE (name, type)
);
-- Associates items with keywords