Correctly increment metadata version when modifying drafts

This commit is contained in:
Ben Gotow 2017-09-25 22:35:25 -07:00
parent a6641dc7da
commit 7b6f8ca81a
14 changed files with 96 additions and 113 deletions

View file

@ -69,7 +69,7 @@ let pluginValue = {
timestamp: 1461361759.351055,
}],
};
messages[0].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue);
messages[0].directlyAttachMetadata(OPEN_TRACKING_ID, pluginValue);
pluginValue = {
links: [{
click_count: 1,
@ -79,24 +79,24 @@ pluginValue = {
}],
tracked: true,
};
messages[0].applyPluginMetadata(LINK_TRACKING_ID, pluginValue);
messages[0].directlyAttachMetadata(LINK_TRACKING_ID, pluginValue);
pluginValue = {
open_count: 1,
open_data: [{
timestamp: 1461361763.283720,
}],
};
messages[1].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue);
messages[1].directlyAttachMetadata(OPEN_TRACKING_ID, pluginValue);
pluginValue = {
links: [],
tracked: false,
};
messages[1].applyPluginMetadata(LINK_TRACKING_ID, pluginValue);
messages[1].directlyAttachMetadata(LINK_TRACKING_ID, pluginValue);
pluginValue = {
open_count: 0,
open_data: [],
};
messages[2].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue);
messages[2].directlyAttachMetadata(OPEN_TRACKING_ID, pluginValue);
pluginValue = {
links: [{
click_count: 0,
@ -104,7 +104,7 @@ pluginValue = {
}],
tracked: true,
};
messages[2].applyPluginMetadata(LINK_TRACKING_ID, pluginValue);
messages[2].directlyAttachMetadata(LINK_TRACKING_ID, pluginValue);
describe('ActivityList', function activityList() {

View file

@ -36,7 +36,9 @@ function forEachATagInBody(draftBodyRootNode, callback) {
export default class LinkTrackingComposerExtension extends ComposerExtension {
static applyTransformsForSending({draftBodyRootNode, draft}) {
const metadata = draft.metadataForPluginId(PLUGIN_ID);
if (metadata) {
if (!metadata) {
return;
}
const messageUid = draft.clientId;
const links = [];
@ -61,8 +63,7 @@ export default class LinkTrackingComposerExtension extends ComposerExtension {
// save the link info to draft metadata
metadata.uid = messageUid;
metadata.links = links;
draft.applyPluginMetadata(PLUGIN_ID, metadata);
}
draft.directlyAttachMetadata(PLUGIN_ID, metadata);
}
static unapplyTransformsForSending({draftBodyRootNode}) {

View file

@ -49,7 +49,7 @@ xdescribe('Link tracking composer extension', function linkTrackingComposerExten
describe("With properly formatted metadata and correct params", () => {
beforeEach(() => {
this.metadata = {tracked: true};
this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata);
this.draft.directlyAttachMetadata(PLUGIN_ID, this.metadata);
});
it("replaces links in the unquoted portion of the body", () => {
@ -84,7 +84,7 @@ xdescribe('Link tracking composer extension', function linkTrackingComposerExten
beforeEach(() => {
this.metadata = {tracked: true, uid: '123'};
this.draft = new Message({accountId: "test"});
this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata);
this.draft.directlyAttachMetadata(PLUGIN_ID, this.metadata);
});
it("takes no action if there are no tracked links in the body", () => {

View file

@ -16,7 +16,7 @@ export default class MessageItemContainer extends React.Component {
message: React.PropTypes.object.isRequired,
messages: React.PropTypes.array.isRequired,
collapsed: React.PropTypes.bool,
isLastItem: React.PropTypes.bool,
isMostRecent: React.PropTypes.bool,
isBeforeReplyArea: React.PropTypes.bool,
scrollTo: React.PropTypes.func,
};
@ -82,7 +82,7 @@ export default class MessageItemContainer extends React.Component {
messages={this.props.messages}
className={this._classNames()}
collapsed={this.props.collapsed}
isLastItem={this.props.isLastItem}
isMostRecent={this.props.isMostRecent}
/>
);
}

View file

@ -27,7 +27,7 @@ export default class MessageItem extends React.Component {
messages: PropTypes.array.isRequired,
collapsed: PropTypes.bool,
pending: PropTypes.bool,
isLastItem: PropTypes.bool,
isMostRecent: PropTypes.bool,
className: PropTypes.string,
};
@ -107,7 +107,7 @@ export default class MessageItem extends React.Component {
}
_onToggleCollapsed = () => {
if (this.props.isLastItem) {
if (this.props.isMostRecent) {
return;
}
Actions.toggleMessageIdExpanded(this.props.message.id);

View file

@ -45,7 +45,7 @@ export default class OpenTrackingComposerExtension extends ComposerExtension {
// save the uid info to draft metadata
metadata.uid = messageUid;
draft.applyPluginMetadata(PLUGIN_ID, metadata);
draft.directlyAttachMetadata(PLUGIN_ID, metadata);
}
static unapplyTransformsForSending({draftBodyRootNode}) {

View file

@ -38,7 +38,7 @@ xdescribe('Open tracking composer extension', function openTrackingComposerExten
describe("With properly formatted metadata and correct params", () => {
beforeEach(() => {
this.metadata = {open_count: 0};
this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata);
this.draft.directlyAttachMetadata(PLUGIN_ID, this.metadata);
OpenTrackingComposerExtension.applyTransformsForSending({
draftBodyRootNode: this.draftBodyRootNode,
@ -72,7 +72,7 @@ xdescribe('Open tracking composer extension', function openTrackingComposerExten
it("removes the image from the body and restore the body to it's exact original content", () => {
this.metadata = {open_count: 0};
this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata);
this.draft.directlyAttachMetadata(PLUGIN_ID, this.metadata);
this.draftBodyRootNode = nodeForHTML(afterBody);
this.draft = new Message({

View file

@ -17,7 +17,7 @@ function find(component, className) {
}
function addOpenMetadata(obj, openCount) {
obj.applyPluginMetadata(PLUGIN_ID, {open_count: openCount});
obj.directlyAttachMetadata(PLUGIN_ID, {open_count: openCount});
}
describe('Open tracking icon', function openTrackingIcon() {
@ -45,7 +45,7 @@ describe('Open tracking icon', function openTrackingIcon() {
});
it("shows no icon if metadata is malformed", () => {
this.messages[0].applyPluginMetadata(PLUGIN_ID, {gar: "bage"});
this.messages[0].directlyAttachMetadata(PLUGIN_ID, {gar: "bage"});
const icon = ReactDOM.findDOMNode(makeIcon(this.thread));
expect(icon.children.length).toEqual(0);
});

View file

@ -12,7 +12,7 @@ function makeIcon(message, props = {}) {
}
function addOpenMetadata(obj, openCount) {
obj.applyPluginMetadata(PLUGIN_ID, {open_count: openCount});
obj.directlyAttachMetadata(PLUGIN_ID, {open_count: openCount});
}
describe('Open tracking message status', function openTrackingMessageStatus() {
@ -28,7 +28,7 @@ describe('Open tracking message status', function openTrackingMessageStatus() {
it("shows nothing if metadata is malformed", () => {
this.message.applyPluginMetadata(PLUGIN_ID, {gar: "bage"});
this.message.directlyAttachMetadata(PLUGIN_ID, {gar: "bage"});
const icon = ReactDOM.findDOMNode(makeIcon(this.message));
expect(icon.querySelector(".open-tracking-message-status")).toBeNull();
});

View file

@ -13,8 +13,8 @@ describe("ModelWithMetadata", function modelWithMetadata() {
describe("metadataForPluginId", () => {
beforeEach(() => {
this.model = new TestModel();
this.model.applyPluginMetadata('plugin-id-a', {a: true});
this.model.applyPluginMetadata('plugin-id-b', {b: false});
this.model.directlyAttachMetadata('plugin-id-a', {a: true});
this.model.directlyAttachMetadata('plugin-id-b', {b: false});
})
it("returns the metadata value for the provided pluginId", () => {
expect(this.model.metadataForPluginId('plugin-id-b')).toEqual({b: false});
@ -27,21 +27,21 @@ describe("ModelWithMetadata", function modelWithMetadata() {
describe("metadataObjectForPluginId", () => {
it("returns the metadata object for the provided pluginId", () => {
const model = new TestModel();
model.applyPluginMetadata('plugin-id-a', {a: true});
model.applyPluginMetadata('plugin-id-b', {b: false});
model.directlyAttachMetadata('plugin-id-a', {a: true});
model.directlyAttachMetadata('plugin-id-b', {b: false});
expect(model.metadataObjectForPluginId('plugin-id-a')).toEqual(model.pluginMetadata[0]);
expect(model.metadataObjectForPluginId('plugin-id-b')).toEqual(model.pluginMetadata[1]);
expect(model.metadataObjectForPluginId('plugin-id-c')).toEqual(undefined);
});
});
describe("applyPluginMetadata", () => {
describe("directlyAttachMetadata", () => {
it("creates or updates the appropriate metadata object", () => {
const model = new TestModel();
expect(model.pluginMetadata.length).toEqual(0);
// create new metadata object with correct value
model.applyPluginMetadata('plugin-id-a', {a: true});
model.directlyAttachMetadata('plugin-id-a', {a: true});
const obj = model.metadataObjectForPluginId('plugin-id-a');
expect(model.pluginMetadata.length).toEqual(1);
expect(obj.pluginId).toBe('plugin-id-a');
@ -50,28 +50,8 @@ describe("ModelWithMetadata", function modelWithMetadata() {
expect(obj.value.a).toBe(true);
// update existing metadata object
model.applyPluginMetadata('plugin-id-a', {a: false});
model.directlyAttachMetadata('plugin-id-a', {a: false});
expect(obj.value.a).toBe(false);
});
});
describe("clonePluginMetadataFrom", () => {
it(`applies the pluginMetadata from the other model, copying values \
but resetting versions`, () => {
const model = new TestModel();
model.applyPluginMetadata('plugin-id-a', {a: true});
model.applyPluginMetadata('plugin-id-b', {b: false});
model.metadataObjectForPluginId('plugin-id-a').version = 2;
model.metadataObjectForPluginId('plugin-id-b').version = 3;
const created = new TestModel();
created.clonePluginMetadataFrom(model);
const aMetadatum = created.metadataObjectForPluginId('plugin-id-a');
const bMetadatum = created.metadataObjectForPluginId('plugin-id-b');
expect(aMetadatum.version).toEqual(0);
expect(aMetadatum.value).toEqual({a: true});
expect(bMetadatum.version).toEqual(0);
expect(bMetadatum.value).toEqual({b: false});
});
});
});

View file

@ -406,8 +406,8 @@ xdescribe('SendDraftTask', function sendDraftTask() {
files: [],
});
this.draft.applyPluginMetadata('pluginIdA', {tracked: true});
this.draft.applyPluginMetadata('pluginIdB', {a: true, b: 2});
this.draft.directlyAttachMetadata('pluginIdA', {tracked: true});
this.draft.directlyAttachMetadata('pluginIdB', {a: true, b: 2});
this.draft.metadataObjectForPluginId('pluginIdA').version = 2;
this.task = new SendDraftTask('client-id');
@ -446,8 +446,8 @@ xdescribe('SendDraftTask', function sendDraftTask() {
files: [],
});
this.draft.applyPluginMetadata('pluginIdA', {tracked: true});
this.draft.applyPluginMetadata('pluginIdB', {a: true, b: 2});
this.draft.directlyAttachMetadata('pluginIdA', {tracked: true});
this.draft.directlyAttachMetadata('pluginIdB', {a: true, b: 2});
this.draft.metadataObjectForPluginId('pluginIdA').version = 2;
this.task = new SendDraftTask('client-id');
@ -477,8 +477,8 @@ xdescribe('SendDraftTask', function sendDraftTask() {
})],
files: [],
});
this.task.draft.applyPluginMetadata('open-tracking', true);
this.task.draft.applyPluginMetadata('link-tracking', true);
this.task.draft.directlyAttachMetadata('open-tracking', true);
this.task.draft.directlyAttachMetadata('link-tracking', true);
this.applySpies = (customValues = {}) => {
let value = {provider: customValues["AccountStore.accountForId"] || "gmail"}
@ -515,20 +515,20 @@ xdescribe('SendDraftTask', function sendDraftTask() {
it("should return false if neither open-tracking nor link-tracking is on", () => {
this.applySpies();
this.task.draft.applyPluginMetadata('open-tracking', false);
this.task.draft.applyPluginMetadata('link-tracking', false);
this.task.draft.directlyAttachMetadata('open-tracking', false);
this.task.draft.directlyAttachMetadata('link-tracking', false);
expect(this.task.hasCustomBodyPerRecipient()).toBe(false);
});
it("should return true if only open-tracking is on", () => {
this.applySpies();
this.task.draft.applyPluginMetadata('link-tracking', false);
this.task.draft.directlyAttachMetadata('link-tracking', false);
expect(this.task.hasCustomBodyPerRecipient()).toBe(true);
});
it("should return true if only link-tracking is on", () => {
this.applySpies();
this.task.draft.applyPluginMetadata('open-tracking', false);
this.task.draft.directlyAttachMetadata('open-tracking', false);
expect(this.task.hasCustomBodyPerRecipient()).toBe(true);
});

View file

@ -5,7 +5,7 @@ import Attributes from '../attributes'
Cloud-persisted data that is associated with a single Nylas API object
(like a `Thread`, `Message`, or `Account`).
*/
class PluginMetadata extends Model {
export class PluginMetadata extends Model {
static attributes = {
pluginId: Attributes.String({
modelKey: 'pluginId',
@ -71,11 +71,31 @@ export default class ModelWithMetadata extends Model {
if (!metadata) {
return null;
}
const m = JSON.parse(JSON.stringify(metadata.value));
if (m.expiration) {
m.expiration = new Date(m.expiration * 1000);
const value = JSON.parse(JSON.stringify(metadata.value));
if (value.expiration) {
value.expiration = new Date(value.expiration * 1000);
}
return m;
return value;
}
/**
* Normally metadata is modified by queueing a SyncbackMetadataTask. We want changes to
* metadata to be undoable just like other draft changes in the composer. To enable this,
* we change the draft's metadata directly with other attributes and then use SyncbackDraftTask
* to commit all the changes at once. It's a bit messy: this code must match the C++ codebase.
*/
directlyAttachMetadata(pluginId, metadataValue) {
let metadata = this.metadataObjectForPluginId(pluginId);
if (!metadata) {
metadata = new PluginMetadata({pluginId, version: 0});
this.pluginMetadata.push(metadata);
}
metadata.value = Object.assign({}, metadataValue);
metadata.version += 1;
if (metadata.value.expiration) {
metadata.value.expiration = Math.round(new Date(metadata.value.expiration).getTime() / 1000);
}
return this;
}
// Private helpers
@ -86,24 +106,4 @@ export default class ModelWithMetadata extends Model {
}
return this.pluginMetadata.find(metadata => metadata.pluginId === pluginId);
}
applyPluginMetadata(pluginId, metadataValue) {
let metadata = this.metadataObjectForPluginId(pluginId);
if (!metadata) {
metadata = new PluginMetadata({pluginId});
this.pluginMetadata.push(metadata);
}
metadata.value = Object.assign({}, metadataValue);
if (metadata.value.expiration) {
metadata.value.expiration = Math.round(new Date(metadata.value.expiration).getTime() / 1000);
}
return this;
}
clonePluginMetadataFrom(otherModel) {
this.pluginMetadata = otherModel.pluginMetadata.map(({pluginId, value}) => {
return new PluginMetadata({pluginId, value});
})
return this;
}
}

View file

@ -4,6 +4,7 @@ import NylasStore from 'nylas-store';
import TaskQueue from './task-queue';
import Message from '../models/message'
import {PluginMetadata} from '../models/model-with-metadata';
import Actions from '../actions'
import AccountStore from './account-store'
import ContactStore from './contact-store'
@ -91,16 +92,17 @@ class DraftChangeSet extends EventEmitter {
}
applyToModel(model) {
if (model) {
if (!model) {
return null;
}
const changesToApply = Object.entries(this._saving).concat(Object.entries(this._pending));
for (const [key, val] of changesToApply) {
if (key.startsWith(MetadataChangePrefix)) {
model.applyPluginMetadata(key.split(MetadataChangePrefix).pop(), val);
model.directlyAttachMetadata(key.split(MetadataChangePrefix).pop(), val);
} else {
model[key] = val;
}
}
}
return model;
}
}

View file

@ -324,7 +324,7 @@ class DraftStore extends NylasStore {
this._doneWithSession(session);
}
// Stop any pending tasks related ot the draft
// Stop any pending tasks related to the draft
TaskQueue.queue().forEach((task) => {
if (task instanceof SyncbackDraftTask && task.headerMessageId === headerMessageId) {
Actions.cancelTask(task);