deleting attributes, closes #34

This commit is contained in:
azivner 2018-02-06 23:09:19 -05:00
parent 7c74c77a2c
commit e011b9ae63
8 changed files with 60 additions and 25 deletions

View file

@ -0,0 +1 @@
ALTER TABLE attributes ADD COLUMN isDeleted INT NOT NULL DEFAULT 0;

View file

@ -86,7 +86,8 @@ CREATE TABLE IF NOT EXISTS "attributes"
name TEXT NOT NULL, name TEXT NOT NULL,
value TEXT, value TEXT,
dateCreated TEXT NOT NULL, dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
); );
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` ( CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`, `entityName`,

View file

@ -24,7 +24,7 @@ class Note extends Entity {
} }
async getAttributes() { async getAttributes() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ?", [this.noteId]); return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
} }
async getAttribute(name) { async getAttribute(name) {

View file

@ -26,6 +26,19 @@ const attributesDialog = (function() {
setTimeout(() => $(".attribute-name:last").focus(), 100); setTimeout(() => $(".attribute-name:last").focus(), 100);
}; };
this.deleteAttribute = function(data, event) {
const attr = self.getTargetAttribute(event);
const attrData = attr();
if (attrData) {
attrData.isDeleted = 1;
attr(attrData);
addLastEmptyRow();
}
};
function isValid() { function isValid() {
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
if (self.isEmptyName(i)) { if (self.isEmptyName(i)) {
@ -65,26 +78,25 @@ const attributesDialog = (function() {
}; };
function addLastEmptyRow() { function addLastEmptyRow() {
const attrs = self.attributes(); const attrs = self.attributes().filter(attr => attr().isDeleted === 0);
const last = attrs.length === 0 ? null : attrs[attrs.length - 1](); const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
if (!last || last.name.trim() !== "" || last.value !== "") { if (!last || last.name.trim() !== "" || last.value !== "") {
self.attributes.push(ko.observable({ self.attributes.push(ko.observable({
attributeId: '', attributeId: '',
name: '', name: '',
value: '' value: '',
isDeleted: 0
})); }));
} }
} }
this.attributeChanged = function (row) { this.attributeChanged = function (data, event) {
addLastEmptyRow(); addLastEmptyRow();
for (const attr of self.attributes()) { const attr = self.getTargetAttribute(event);
if (row.attributeId === attr().attributeId) {
attr.valueHasMutated(); attr.valueHasMutated();
}
}
}; };
this.isNotUnique = function(index) { this.isNotUnique = function(index) {
@ -109,6 +121,13 @@ const attributesDialog = (function() {
const cur = self.attributes()[index](); const cur = self.attributes()[index]();
return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== ""); return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== "");
};
this.getTargetAttribute = function(event) {
const context = ko.contextFor(event.target);
const index = context.$index();
return self.attributes()[index];
} }
} }

View file

@ -12,7 +12,7 @@ const attributes = require('../../services/attributes');
router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId; const noteId = req.params.noteId;
res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId])); res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY dateCreated", [noteId]));
})); }));
router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
@ -23,10 +23,15 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res,
await sql.doInTransaction(async () => { await sql.doInTransaction(async () => {
for (const attr of attributes) { for (const attr of attributes) {
if (attr.attributeId) { if (attr.attributeId) {
await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ? WHERE attributeId = ?", await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ?, isDeleted = ? WHERE attributeId = ?",
[attr.name, attr.value, now, attr.attributeId]); [attr.name, attr.value, now, attr.isDeleted, attr.attributeId]);
} }
else { else {
// if it was "created" and then immediatelly deleted, we just don't create it at all
if (attr.isDeleted) {
continue;
}
attr.attributeId = utils.newAttributeId(); attr.attributeId = utils.newAttributeId();
await sql.insert("attributes", { await sql.insert("attributes", {
@ -35,7 +40,8 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res,
name: attr.name, name: attr.name,
value: attr.value, value: attr.value,
dateCreated: now, dateCreated: now,
dateModified: now dateModified: now,
isDeleted: false
}); });
} }
@ -43,11 +49,11 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res,
} }
}); });
res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId])); res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY dateCreated", [noteId]));
})); }));
router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => {
const names = await sql.getColumn("SELECT DISTINCT name FROM attributes"); const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0");
for (const attr of attributes.BUILTIN_ATTRIBUTES) { for (const attr of attributes.BUILTIN_ATTRIBUTES) {
if (!names.includes(attr)) { if (!names.includes(attr)) {
@ -63,7 +69,7 @@ router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) =
router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => { router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => {
const attributeName = req.params.attributeName; const attributeName = req.params.attributeName;
const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE name = ? AND value != '' ORDER BY value", [attributeName]); const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [attributeName]);
res.send(values); res.send(values);
})); }));

View file

@ -3,7 +3,7 @@
const build = require('./build'); const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const APP_DB_VERSION = 72; const APP_DB_VERSION = 73;
module.exports = { module.exports = {
app_version: packageJson.version, app_version: packageJson.version,

View file

@ -13,7 +13,10 @@ async function getNoteAttributeMap(noteId) {
async function getNoteIdWithAttribute(name, value) { async function getNoteIdWithAttribute(name, value) {
return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId) return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]); WHERE notes.isDeleted = 0
AND attributes.isDeleted = 0
AND attributes.name = ?
AND attributes.value = ?`, [name, value]);
} }
async function getNotesWithAttribute(dataKey, name, value) { async function getNotesWithAttribute(dataKey, name, value) {
@ -23,11 +26,11 @@ async function getNotesWithAttribute(dataKey, name, value) {
if (value !== undefined) { if (value !== undefined) {
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]); WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
} }
else { else {
notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]); WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]);
} }
return notes; return notes;
@ -41,7 +44,7 @@ async function getNoteWithAttribute(dataKey, name, value) {
async function getNoteIdsWithAttribute(name) { async function getNoteIdsWithAttribute(name) {
return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId) return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]); WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]);
} }
async function createAttribute(noteId, name, value = null, sourceId = null) { async function createAttribute(noteId, name, value = null, sourceId = null) {
@ -54,7 +57,8 @@ async function createAttribute(noteId, name, value = null, sourceId = null) {
name: name, name: name,
value: value, value: value,
dateModified: now, dateModified: now,
dateCreated: now dateCreated: now,
isDeleted: false
}); });
await sync_table.addAttributeSync(attributeId, sourceId); await sync_table.addAttributeSync(attributeId, sourceId);

View file

@ -376,7 +376,7 @@
<div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;"> <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
<form data-bind="submit: save"> <form data-bind="submit: save">
<div style="text-align: center"> <div style="text-align: center">
<button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save <kbd>enter</kbd></button> <button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button>
</div> </div>
<div style="height: 97%; overflow: auto"> <div style="height: 97%; overflow: auto">
@ -386,10 +386,11 @@
<th>ID</th> <th>ID</th>
<th>Name</th> <th>Name</th>
<th>Value</th> <th>Value</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody data-bind="foreach: attributes"> <tbody data-bind="foreach: attributes">
<tr> <tr data-bind="if: isDeleted == 0">
<td data-bind="text: attributeId"></td> <td data-bind="text: attributeId"></td>
<td> <td>
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event --> <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
@ -400,6 +401,9 @@
<td> <td>
<input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/> <input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
</td> </td>
<td title="Delete" style="padding: 13px;">
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>