mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Started synchronizer
This commit is contained in:
parent
0b77715a93
commit
db4b6cf0d6
@ -5,6 +5,11 @@ import { uuid } from 'src/uuid.js';
|
||||
|
||||
class BaseModel {
|
||||
|
||||
static ITEM_TYPE_NOTE = 1;
|
||||
static ITEM_TYPE_FOLDER = 2;
|
||||
static tableInfo_ = null;
|
||||
static tableKeys_ = null;
|
||||
|
||||
static tableName() {
|
||||
throw new Error('Must be overriden');
|
||||
}
|
||||
@ -13,6 +18,14 @@ class BaseModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
static itemType() {
|
||||
throw new Error('Must be overriden');
|
||||
}
|
||||
|
||||
static trackChanges() {
|
||||
return false;
|
||||
}
|
||||
|
||||
static byId(items, id) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].id == id) return items[i];
|
||||
@ -20,12 +33,31 @@ class BaseModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
static save(o) {
|
||||
let isNew = !o.id;
|
||||
static fieldNames() {
|
||||
return this.db().tableFieldNames(this.tableName());
|
||||
}
|
||||
|
||||
static fromApiResult(apiResult) {
|
||||
let fieldNames = this.fieldNames();
|
||||
let output = {};
|
||||
for (let i = 0; i < fieldNames.length; i++) {
|
||||
let f = fieldNames[i];
|
||||
output[f] = f in apiResult ? apiResult[f] : null;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static saveQuery(o, isNew = 'auto') {
|
||||
if (isNew == 'auto') isNew = !o.id;
|
||||
let query = '';
|
||||
let itemId = o.id;
|
||||
|
||||
if (isNew) {
|
||||
if (this.useUuid()) o.id = uuid.create();
|
||||
if (this.useUuid()) {
|
||||
o = Object.assign({}, o);
|
||||
itemId = uuid.create();
|
||||
o.id = itemId;
|
||||
}
|
||||
query = Database.insertQuery(this.tableName(), o);
|
||||
} else {
|
||||
let where = { id: o.id };
|
||||
@ -34,7 +66,44 @@ class BaseModel {
|
||||
query = Database.updateQuery(this.tableName(), temp, where);
|
||||
}
|
||||
|
||||
return this.db().exec(query.sql, query.params).then(() => { return o; });
|
||||
query.id = itemId;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
static save(o, trackChanges = true, isNew = 'auto') {
|
||||
if (isNew == 'auto') isNew = !o.id;
|
||||
let query = this.saveQuery(o, isNew);
|
||||
|
||||
return this.db().transaction((tx) => {
|
||||
tx.executeSql(query.sql, query.params);
|
||||
|
||||
if (trackChanges && this.trackChanges()) {
|
||||
// Cannot import this class the normal way due to cyclical dependencies between Change and BaseModel
|
||||
// which are not handled by React Native.
|
||||
const { Change } = require('src/models/change.js');
|
||||
|
||||
let change = Change.newChange();
|
||||
change.type = isNew ? Change.TYPE_CREATE : Change.TYPE_UPDATE;
|
||||
change.item_id = query.id;
|
||||
change.item_type = this.itemType();
|
||||
|
||||
let changeQuery = Change.saveQuery(change);
|
||||
tx.executeSql(changeQuery.sql, changeQuery.params);
|
||||
|
||||
// TODO: item field for UPDATE
|
||||
}
|
||||
}).then(() => {
|
||||
o = Object.assign({}, o);
|
||||
o.id = query.id;
|
||||
|
||||
this.dispatch({
|
||||
type: 'FOLDERS_UPDATE_ONE',
|
||||
folder: o,
|
||||
});
|
||||
|
||||
return o;
|
||||
});
|
||||
}
|
||||
|
||||
static delete(id) {
|
||||
|
@ -29,17 +29,6 @@ class NotesScreenComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
loginButton_press = () => {
|
||||
this.props.dispatch({
|
||||
type: 'Navigation/NAVIGATE',
|
||||
routeName: 'Login',
|
||||
});
|
||||
}
|
||||
|
||||
syncButton_press = () => {
|
||||
Log.info('SYNC');
|
||||
}
|
||||
|
||||
deleteFolder_onPress = (folderId) => {
|
||||
Folder.delete(folderId).then(() => {
|
||||
this.props.dispatch({
|
||||
@ -77,10 +66,6 @@ class NotesScreenComponent extends React.Component {
|
||||
<View style={{flex: 1}}>
|
||||
<ScreenHeader title={title} navState={this.props.navigation.state} menuOptions={this.menuOptions()} />
|
||||
<NoteList style={{flex: 1}}/>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<Button title="Login" onPress={this.loginButton_press} />
|
||||
<Button title="Sync" onPress={this.syncButton_press} />
|
||||
</View>
|
||||
<ActionButton parentFolderId={this.props.selectedFolderId}></ActionButton>
|
||||
</View>
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import SQLite from 'react-native-sqlite-storage';
|
||||
import { Log } from 'src/log.js';
|
||||
import { uuid } from 'src/uuid.js';
|
||||
import { PromiseChain } from 'src/promise-chain.js';
|
||||
|
||||
const structureSql = `
|
||||
CREATE TABLE folders (
|
||||
@ -78,6 +79,12 @@ CREATE TABLE settings (
|
||||
\`type\` INT
|
||||
);
|
||||
|
||||
CREATE TABLE table_fields (
|
||||
id INTEGER PRIMARY KEY,
|
||||
table_name TEXT,
|
||||
field_name TEXT
|
||||
);
|
||||
|
||||
INSERT INTO version (version) VALUES (1);
|
||||
`;
|
||||
|
||||
@ -86,6 +93,7 @@ class Database {
|
||||
constructor() {
|
||||
this.debugMode_ = false;
|
||||
this.initialized_ = false;
|
||||
this.tableFields_ = null;
|
||||
}
|
||||
|
||||
setDebugEnabled(v) {
|
||||
@ -102,13 +110,13 @@ class Database {
|
||||
}
|
||||
|
||||
open() {
|
||||
this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-7.sqlite' }, (db) => {
|
||||
this.db_ = SQLite.openDatabase({ name: '/storage/emulated/0/Download/joplin-10.sqlite' }, (db) => {
|
||||
Log.info('Database was open successfully');
|
||||
}, (error) => {
|
||||
Log.error('Cannot open database: ', error);
|
||||
});
|
||||
|
||||
return this.updateSchema();
|
||||
return this.initialize();
|
||||
}
|
||||
|
||||
static enumToId(type, s) {
|
||||
@ -119,6 +127,12 @@ class Database {
|
||||
throw new Error('Unknown enum type or value: ' + type + ', ' + s);
|
||||
}
|
||||
|
||||
tableFieldNames(tableName) {
|
||||
if (!this.tableFields_) throw new Error('Fields have not been loaded yet');
|
||||
if (!this.tableFields_[tableName]) throw new Error('Unknown table: ' + tableName);
|
||||
return this.tableFields_[tableName];
|
||||
}
|
||||
|
||||
sqlStringToLines(sql) {
|
||||
let output = [];
|
||||
let lines = sql.split("\n");
|
||||
@ -138,7 +152,7 @@ class Database {
|
||||
|
||||
logQuery(sql, params = null) {
|
||||
if (!this.debugMode()) return;
|
||||
Log.debug('DB: ' + sql, params);
|
||||
//Log.debug('DB: ' + sql, params);
|
||||
}
|
||||
|
||||
selectOne(sql, params = null) {
|
||||
@ -220,34 +234,119 @@ class Database {
|
||||
});
|
||||
}
|
||||
|
||||
updateSchema() {
|
||||
refreshTableFields() {
|
||||
return this.exec('SELECT name FROM sqlite_master WHERE type="table"').then((tableResults) => {
|
||||
let chain = [];
|
||||
for (let i = 0; i < tableResults.rows.length; i++) {
|
||||
let row = tableResults.rows.item(i);
|
||||
let tableName = row.name;
|
||||
if (tableName == 'android_metadata') continue;
|
||||
if (tableName == 'table_fields') continue;
|
||||
|
||||
chain.push((queries) => {
|
||||
if (!queries) queries = [];
|
||||
return this.exec('PRAGMA table_info("' + tableName + '")').then((pragmaResult) => {
|
||||
for (let i = 0; i < pragmaResult.rows.length; i++) {
|
||||
let q = Database.insertQuery('table_fields', {
|
||||
table_name: tableName,
|
||||
field_name: pragmaResult.rows.item(i).name,
|
||||
});
|
||||
queries.push(q);
|
||||
}
|
||||
return queries;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return PromiseChain.exec(chain).then((queries) => {
|
||||
return this.transaction((tx) => {
|
||||
tx.executeSql('DELETE FROM table_fields');
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
tx.executeSql(queries[i].sql, queries[i].params);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initialize() {
|
||||
Log.info('Checking for database schema update...');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.selectOne('SELECT * FROM version LIMIT 1').then((row) => {
|
||||
return this.selectOne('SELECT * FROM version LIMIT 1').then((row) => {
|
||||
Log.info('Current database version', row);
|
||||
resolve();
|
||||
// TODO: version update logic
|
||||
|
||||
// TODO: only do this if db has been updated:
|
||||
return this.refreshTableFields();
|
||||
}).then(() => {
|
||||
return this.exec('SELECT * FROM table_fields').then((r) => {
|
||||
this.tableFields_ = {};
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
let row = r.rows.item(i);
|
||||
if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = [];
|
||||
this.tableFields_[row.table_name].push(row.field_name);
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
// Assume that error was:
|
||||
// { message: 'no such table: version (code 1): , while compiling: SELECT * FROM version', code: 0 }
|
||||
// which means the database is empty and the tables need to be created.
|
||||
// If it's any other error there's nothing we can do anyway.
|
||||
|
||||
Log.info('Database is new - creating the schema...');
|
||||
|
||||
let statements = this.sqlStringToLines(structureSql)
|
||||
this.transaction((tx) => {
|
||||
return this.transaction((tx) => {
|
||||
for (let i = 0; i < statements.length; i++) {
|
||||
tx.executeSql(statements[i]);
|
||||
}
|
||||
tx.executeSql('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumToId('settings', 'string') + '")');
|
||||
}).then(() => {
|
||||
resolve('Database schema created successfully');
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
Log.info('Database schema created successfully');
|
||||
// Calling initialize() now that the db has been created will make it go through
|
||||
// the normal db update process (applying any additional patch).
|
||||
return this.initialize();
|
||||
})
|
||||
});
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// this.selectOne('SELECT * FROM version LIMIT 1').then((row) => {
|
||||
// Log.info('Current database version', row);
|
||||
// // TODO: version update logic
|
||||
// // TODO: only do this if db has been updated
|
||||
// return this.refreshTableFields();
|
||||
// }).then(() => {
|
||||
// return this.exec('SELECT * FROM table_fields').then((r) => {
|
||||
// this.tableFields_ = {};
|
||||
// for (let i = 0; i < r.rows.length; i++) {
|
||||
// let row = r.rows.item(i);
|
||||
// if (!this.tableFields_[row.table_name]) this.tableFields_[row.table_name] = [];
|
||||
// this.tableFields_[row.table_name].push(row.field_name);
|
||||
// }
|
||||
// });
|
||||
// }).catch((error) => {
|
||||
// // Assume that error was:
|
||||
// // { message: 'no such table: version (code 1): , while compiling: SELECT * FROM version', code: 0 }
|
||||
// // which means the database is empty and the tables need to be created.
|
||||
|
||||
// Log.info('Database is new - creating the schema...');
|
||||
|
||||
// let statements = this.sqlStringToLines(structureSql)
|
||||
// this.transaction((tx) => {
|
||||
// for (let i = 0; i < statements.length; i++) {
|
||||
// tx.executeSql(statements[i]);
|
||||
// }
|
||||
// tx.executeSql('INSERT INTO settings (`key`, `value`, `type`) VALUES ("clientId", "' + uuid.create() + '", "' + Database.enumToId('settings', 'string') + '")');
|
||||
// }).then(() => {
|
||||
// Log.info('Database schema created successfully');
|
||||
// // Calling initialize() now that the db has been created will make it go through
|
||||
// // the normal db update process (applying any additional patch).
|
||||
// return this.initialize();
|
||||
// }).catch((error) => {
|
||||
// reject(error);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
}
|
||||
|
37
ReactNativeClient/src/models/change.js
Normal file
37
ReactNativeClient/src/models/change.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { BaseModel } from 'src/base-model.js';
|
||||
import { Log } from 'src/log.js';
|
||||
|
||||
class Change extends BaseModel {
|
||||
|
||||
static TYPE_UNKNOWN = 0;
|
||||
static TYPE_CREATE = 1;
|
||||
static TYPE_UPDATE = 2;
|
||||
static TYPE_DELETE = 3;
|
||||
|
||||
static tableName() {
|
||||
return 'changes';
|
||||
}
|
||||
|
||||
static newChange() {
|
||||
return {
|
||||
id: null,
|
||||
type: null,
|
||||
item_id: null,
|
||||
item_type: null,
|
||||
item_field: null,
|
||||
};
|
||||
}
|
||||
|
||||
// static all() {
|
||||
// return this.db().selectAll('SELECT * FROM folders').then((r) => {
|
||||
// let output = [];
|
||||
// for (let i = 0; i < r.rows.length; i++) {
|
||||
// output.push(r.rows.item(i));
|
||||
// }
|
||||
// return output;
|
||||
// });
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
export { Change };
|
@ -11,6 +11,14 @@ class Folder extends BaseModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
static itemType() {
|
||||
return BaseModel.ITEM_TYPE_FOLDER;
|
||||
}
|
||||
|
||||
static trackChanges() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static newFolder() {
|
||||
return {
|
||||
id: null,
|
||||
|
@ -11,6 +11,14 @@ class Note extends BaseModel {
|
||||
return true;
|
||||
}
|
||||
|
||||
static itemType() {
|
||||
return BaseModel.ITEM_TYPE_NOTE;
|
||||
}
|
||||
|
||||
static trackChanges() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static newNote(parentId = null) {
|
||||
return {
|
||||
id: null,
|
||||
|
@ -7,9 +7,9 @@ class Setting extends BaseModel {
|
||||
static defaults_ = {
|
||||
'clientId': { value: '', type: 'string' },
|
||||
'sessionId': { value: '', type: 'string' },
|
||||
'lastUpdateTime': { value: '', type: 'int' },
|
||||
'user.email': { value: '', type: 'string' },
|
||||
'user.session': { value: '', type: 'string' },
|
||||
'sync.lastRevId': { value: 0, type: 'int' },
|
||||
};
|
||||
|
||||
static tableName() {
|
||||
|
14
ReactNativeClient/src/promise-chain.js
Normal file
14
ReactNativeClient/src/promise-chain.js
Normal file
@ -0,0 +1,14 @@
|
||||
class PromiseChain {
|
||||
|
||||
static exec(chain) {
|
||||
let output = new Promise((resolve, reject) => { resolve(); });
|
||||
for (let i = 0; i < chain.length; i++) {
|
||||
let f = chain[i];
|
||||
output = output.then(f);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { PromiseChain };
|
@ -9,6 +9,7 @@ import { addNavigationHelpers } from 'react-navigation';
|
||||
import { Log } from 'src/log.js'
|
||||
import { Note } from 'src/models/note.js'
|
||||
import { Folder } from 'src/models/folder.js'
|
||||
import { BaseModel } from 'src/base-model.js'
|
||||
import { Database } from 'src/database.js'
|
||||
import { Registry } from 'src/registry.js'
|
||||
import { ItemList } from 'src/components/item-list.js'
|
||||
@ -18,6 +19,7 @@ import { FolderScreen } from 'src/components/screens/folder.js'
|
||||
import { FoldersScreen } from 'src/components/screens/folders.js'
|
||||
import { LoginScreen } from 'src/components/screens/login.js'
|
||||
import { Setting } from 'src/models/setting.js'
|
||||
import { Synchronizer } from 'src/synchronizer.js'
|
||||
import { MenuContext } from 'react-native-popup-menu';
|
||||
|
||||
let defaultState = {
|
||||
@ -163,6 +165,8 @@ class AppComponent extends React.Component {
|
||||
let db = new Database();
|
||||
db.setDebugEnabled(Registry.debugMode());
|
||||
|
||||
BaseModel.dispatch = this.props.dispatch;
|
||||
|
||||
db.open().then(() => {
|
||||
Log.info('Database is ready.');
|
||||
Registry.setDb(db);
|
||||
@ -174,6 +178,8 @@ class AppComponent extends React.Component {
|
||||
Log.info('Client ID', Setting.value('clientId'));
|
||||
Log.info('User', user);
|
||||
|
||||
Registry.api().setSession(user.session);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'USER_SET',
|
||||
user: user,
|
||||
@ -189,8 +195,11 @@ class AppComponent extends React.Component {
|
||||
}).catch((error) => {
|
||||
Log.warn('Cannot load folders', error);
|
||||
});
|
||||
}).then(() => {
|
||||
let synchronizer = new Synchronizer();
|
||||
synchronizer.start();
|
||||
}).catch((error) => {
|
||||
Log.error('Cannot initialize database:', error);
|
||||
Log.error('Initialization error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
|
70
ReactNativeClient/src/synchronizer.js
Normal file
70
ReactNativeClient/src/synchronizer.js
Normal file
@ -0,0 +1,70 @@
|
||||
import { Registry } from 'src/registry.js';
|
||||
import { Log } from 'src/log.js';
|
||||
import { Setting } from 'src/models/setting.js';
|
||||
import { Change } from 'src/models/change.js';
|
||||
import { Folder } from 'src/models/folder.js';
|
||||
|
||||
class Synchronizer {
|
||||
|
||||
constructor() {
|
||||
this.state_ = 'idle';
|
||||
}
|
||||
|
||||
state() {
|
||||
return this.state_;
|
||||
}
|
||||
|
||||
db() {
|
||||
return Registry.db();
|
||||
}
|
||||
|
||||
api() {
|
||||
return Registry.api();
|
||||
}
|
||||
|
||||
switchState(state) {
|
||||
Log.info('Sync: switching state to: ' + state);
|
||||
|
||||
if (state == 'downloadChanges') {
|
||||
this.api().get('synchronizer', { last_id: Setting.value('sync.lastRevId') }).then((syncOperations) => {
|
||||
let promise = new Promise((resolve, reject) => { resolve(); });
|
||||
for (let i = 0; i < syncOperations.items.length; i++) {
|
||||
let syncOp = syncOperations.items[i];
|
||||
if (syncOp.item_type == 'folder') {
|
||||
if (syncOp.type == 'create') {
|
||||
promise = promise.then(() => {
|
||||
let folder = Folder.fromApiResult(syncOp.item);
|
||||
// TODO: automatically handle NULL fields by checking type and default value of field
|
||||
if (!folder.parent_id) folder.parent_id = '';
|
||||
return Folder.save(folder, true, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
promise.then(() => {
|
||||
Log.info('All items synced.');
|
||||
}).catch((error) => {
|
||||
Log.warn('Sync error', error);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
|
||||
if (this.state() != 'idle') {
|
||||
Log.info("Sync: cannot start synchronizer because synchronization already in progress. State: " + this.state());
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info('Sync: start');
|
||||
|
||||
this.switchState('downloadChanges');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Synchronizer };
|
@ -5,6 +5,15 @@ class WebApi {
|
||||
|
||||
constructor(baseUrl) {
|
||||
this.baseUrl_ = baseUrl;
|
||||
this.session_ = null;
|
||||
}
|
||||
|
||||
setSession(v) {
|
||||
this.session_ = v;
|
||||
}
|
||||
|
||||
session() {
|
||||
return this.session_;
|
||||
}
|
||||
|
||||
makeRequest(method, path, query, data) {
|
||||
@ -38,14 +47,18 @@ class WebApi {
|
||||
if (o.method != 'GET' && o.method != 'DELETE') {
|
||||
cmd.push("--data '" + stringify(data) + "'");
|
||||
}
|
||||
cmd.push(r.url);
|
||||
cmd.push("'" + r.url + "'");
|
||||
return cmd.join(' ');
|
||||
}
|
||||
|
||||
exec(method, path, query, data) {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
let r = that.makeRequest(method, path, query, data);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.session_) {
|
||||
query = query ? Object.assign({}, query) : {};
|
||||
if (!query.session) query.session = this.session_;
|
||||
}
|
||||
|
||||
let r = this.makeRequest(method, path, query, data);
|
||||
|
||||
Log.debug(WebApi.toCurl(r, data));
|
||||
|
||||
|
@ -26,7 +26,7 @@ function config($name) {
|
||||
'baseUrl' => $baseUrl,
|
||||
'clientId' => 'E3E3E3E3E3E3E3E3E3E3E3E3E3E3E3E3',
|
||||
'email' => 'laurent@cozic.net',
|
||||
'password' => '123456789',
|
||||
'password' => '12345678',
|
||||
);
|
||||
if (isset($config[$name])) return $config[$name];
|
||||
throw new Exception('Unknown config: ' . $name);
|
||||
|
@ -381,7 +381,7 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
||||
|
||||
if ($this->isVersioned) {
|
||||
if (count($changedFields)) {
|
||||
$this->recordChanges($isNew ? 'create' : 'update', $changedFields);
|
||||
$this->trackChanges($isNew ? 'create' : 'update', $changedFields);
|
||||
}
|
||||
$this->changedDiffableFields = array();
|
||||
}
|
||||
@ -395,13 +395,13 @@ class BaseModel extends \Illuminate\Database\Eloquent\Model {
|
||||
$output = parent::delete();
|
||||
|
||||
if (count($this->isVersioned)) {
|
||||
$this->recordChanges('delete');
|
||||
$this->trackChanges('delete');
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function recordChanges($type, $changedFields = array()) {
|
||||
protected function trackChanges($type, $changedFields = array()) {
|
||||
if ($type == 'delete') {
|
||||
$change = $this->newChange($type);
|
||||
$change->save();
|
||||
|
@ -46,6 +46,7 @@ class Change extends BaseModel {
|
||||
$itemIdToChange[$change->item_id] = $change;
|
||||
}
|
||||
|
||||
|
||||
$output = array();
|
||||
foreach ($itemIdToChange as $itemId => $change) {
|
||||
if (in_array($itemId, $createdItems) && in_array($itemId, $deletedItems)) {
|
||||
|
Loading…
Reference in New Issue
Block a user