local DB row store on frontend

stable
brian 4 years ago
parent b75e127625
commit 73f5e16f30

@ -1,6 +1,7 @@
import Note from './Note.js';
import {
new_note_handler,
new_note_by_dblclick_handler,
search_term_change_handler,
sorting_change_handler,
load_notes,
@ -38,7 +39,7 @@ function App(vnode_init){
(s.login.showing
? (s.login.is_logged_in
? m('.login-info', {key:'login-info'}, [
m('.text', 'Logged in as '+s.login.user.username),
m('.text', 'Logged in as '+s.db[s.login.user_id].model.username),
m('button', {onclick: o(logout_request_handler)}, 'Logout')
])
: m('.login-form', {key:'login-form'}, [
@ -52,7 +53,7 @@ function App(vnode_init){
m('.buttons', {key:'buttons'}, [
(s.is_editing_bin_name
? m('input.bin-name-textbox', {key: 'bin-name-textbox', value: s.temp_bin_name, onchange: o(bin_name_change_handler)})
: m('.bin-name', {key: 'bin-name'}, s.bin.name)
: m('.bin-name', {key: 'bin-name'}, s.db[s.bin_id].model.name)
),
m('button', {key: 'new-bin-button', onclick: o(new_bin_handler)}, 'New Bin...'),
(s.is_editing_bin_name
@ -60,14 +61,14 @@ function App(vnode_init){
: m('button', {key:'rename-bin-button', onclick: o(bin_name_editing_toggle_button_handler)}, 'Rename')
)
]),
m('ul.bin-list', {key:'bin-list'}, s.login.bins.map(b=>
m('li.bin', {key:b.id, onclick:o1(choose_bin_handler, b.id)}, b.name)
m('ul.bin-list', {key:'bin-list'}, s.login.bin_ids.map(bin_id=>
m('li.bin', {key:bin_id, onclick:o1(choose_bin_handler, bin_id)}, s.db[bin_id].model.name)
))
])
]),
m('.main', {key: 'main'}, [
m('.notes', s.notes.map(note_state =>
m(Note, {state:s, note_state, dispatch, key: note_state.note.id})
m('.notes', {ondblclick: o(new_note_by_dblclick_handler)}, s.notes.map(note_state =>
m(Note, {state:s, note_state, dispatch, key: note_state.note_id})
))
])
]);

@ -4,8 +4,9 @@ function Note(vnode_init){
const {dispatch} = vnode_init.attrs;
return {
view: function(vnode){
const {note_state} = vnode.attrs;
const {is_editing, is_focused, note} = note_state;
const {state, note_state} = vnode.attrs;
const {is_editing, is_focused, note_id} = note_state;
const note = state.db[note_id].model;
if(is_editing){
return m('.note', [
m('textarea', {key: 'textarea', onchange: text_change_handler.bind(null, note_state, dispatch), oncreate: is_focused?({dom})=>{ dom.focus(); delete note_state.is_focused; }:null }, note_state.temp_text),

@ -2,13 +2,13 @@ import nanoid from '../nanoid.min.js';
import api from '../api.js';
const load_notes = function(state, dispatch){
api.post('/load-notes', {bin_id: state.bin.id})
api.post('/load-notes', {bin_id: state.bin_id})
.then(res=>{
dispatch('notes-loaded', res.notes);
});
};
const runSearch = function(state, dispatch){
api.post('/search', {search_term: state.search_term, sorting: state.sorting, bin_id: state.bin.id})
api.post('/search', {search_term: state.search_term, sorting: state.sorting, bin_id: state.bin_id})
.then(res=>{
dispatch('update-search-results', res.notes);
});
@ -16,6 +16,10 @@ const runSearch = function(state, dispatch){
const new_note_handler = function(state, dispatch){
dispatch('add-note', {id: nanoid(), date: Date.now()});
};
const new_note_by_dblclick_handler = function(state, dispatch, e){
e.preventDefault(); // for nothing gets highlighted
dispatch('add-note', {id: nanoid(), date: Date.now()});
};
const search_term_change_handler = function(state, dispatch, e){
if(e.code === 'Enter'){
runSearch(state, dispatch);
@ -51,8 +55,8 @@ const login_request_handler = function(state, dispatch, e){
else{
dispatch('login-failed');
}
})
.catch(e=>{ dispatch('login-failed'); });
},
e=>{ dispatch('login-failed'); });
};
const logout_request_handler = function(state, dispatch, e){
api.post('/logout', {});
@ -68,7 +72,7 @@ const bin_name_change_handler = function(state, dispatch, e){
dispatch('update-bin-name', e.target.value);
};
const bin_name_commit_handler = function(state, dispatch){
api.post('/bin-rename', {bin_id:state.bin.id, name:state.temp_bin_name});
api.post('/bin-rename', {bin_id:state.bin_id, name:state.temp_bin_name});
dispatch('commit-bin-name');
};
const choose_bin_handler = function(state, dispatch, bin_id){
@ -85,6 +89,7 @@ const choose_bin_handler = function(state, dispatch, bin_id){
};
export {new_note_handler,
new_note_by_dblclick_handler,
search_term_change_handler,
sorting_change_handler,
load_notes,

@ -1,17 +1,17 @@
import api from '../api.js';
const edit_handler = function(note_state, dispatch){
dispatch('update-note-editing', {id: note_state.note.id, is_editing: true});
dispatch('update-note-editing', {id: note_state.note_id, is_editing: true});
};
const cancel_handler = function(note_state, dispatch){
dispatch('update-note-editing', {id: note_state.note.id, is_editing: false});
dispatch('update-note-editing', {id: note_state.note_id, is_editing: false});
};
const text_change_handler = function(note_state, dispatch, e){
note_state.temp_text = e.target.value;
};
const save_handler = function(note_state, dispatch){
dispatch('save-note-edit', {id: note_state.note.id, text: note_state.temp_text});
api.post('/save', {bin_id: note_state.bin_id, note_id: note_state.note.id, text: note_state.temp_text})
dispatch('save-note-edit', {id: note_state.note_id, text: note_state.temp_text});
api.post('/save', {bin_id: note_state.bin_id, note_id: note_state.note_id, text: note_state.temp_text})
};
export { edit_handler, cancel_handler, text_change_handler, save_handler };

@ -15,7 +15,7 @@ const hash_change_handler = function(state, dispatch, e){
// get bin id from URL
let bin_id = window.location.hash.substring(1); // extract the leading '#'
if(bin_id === ''){
const old_bin_id = state.bin.id;
const old_bin_id = state.bin_id;
window.history.replaceState(null,'', '#'+old_bin_id);
}
else{

@ -24,45 +24,100 @@ if(bin_id === ''){
}
// the actual loading from server is done later, after store and dispatch are defined
/* Integrates a database row from the server into the local store: */
const integrate = function(s, model_object){
const id = model_object.id;
s.db[id] = s.db[id] || {ref_count: 0, model: null};
s.db[id].model = model_object;
};
const i = integrate;
/* integrates a list of model objects, as if 'integrate' were called on each element. */
const integrateAll = function(s, model_objects){
model_objects.forEach(mo=>{
integrate(s, mo);
});
};
/* Register a reference-by-id: */
const ref = function(s, o, key, new_model_id){
const old_model_id = o[key];
s.db[new_model_id].ref_count++;
if(old_model_id && s.db[old_model_id]){
s.db[old_model_id].ref_count--;
if(s.db[old_model_id].ref_count === 0){
delete s.db[old_model_id];
}
}
// this needs to come after the above 'if' statement:
o[key] = new_model_id;
};
/* Register references in-bulk: */
/* o[key] refers to an array-of-ids */
const refAll = function(s, o, key, new_model_ids){
const old_model_ids = o[key];
// ref the new refs:
o[key] = new_model_ids;
new_model_ids.forEach(new_model_id=>{
s.db[new_model_id].ref_count++;
});
// de-ref the existing refs, deleting no-longer-referred-to models:
old_model_ids.forEach(old_model_id=>{
s.db[old_model_id].ref_count--;
// if this is the last reference, delete it from the store:
if(s.db[old_model_id].ref_count === 0){
delete s.db[old_model_id];
}
});
};
/* TODO: routine to de-ref and ref in bulk when the id is in an object which is in an array (as opposed to refAll, where the id itself is in the array)
This routine would need to be called for state.notes, which contains refs of the form: `state.notes[n].note_id`
*/
function addToBinListIfLoggedIn(state){
const s = state;
// if user is logged in:
if(s.login.is_logged_in){
// if bin is not already in the list:
if(s.login.bins.filter(b=>b.id===s.bin.id).length===0){
s.login.bins.push({id: s.bin.id, name: s.bin.name});
if(s.login.bin_ids.filter(bin_id=>bin_id===s.bin_id).length===0){
i(s, {id: s.bin_id, name: s.db[s.bin_id].model.name});
s.login.bin_ids.push(s.bin_id);
}
}
}
const reducer = handleActions({
'new-bin': (s, bin) => { s.bin=bin; s.notes=[]; s.temp_bin_name=bin.id; },
'bin-requested': (s, bin_id) => { s.bin={id:bin_id}; s.notes = []; },
'bin-loaded': (s, bin) => { s.bin=bin; s.temp_bin_name=bin.name; },
'new-bin': (s, bin) => { i(s,bin); ref(s,s,'bin_id',bin.id); s.notes=[]; s.temp_bin_name=bin.id; },
'bin-requested': (s, bin_id) => { i(s,{id:bin_id}); ref(s,s,'bin_id',bin_id); s.notes = []; },
'bin-loaded': (s, bin) => { i(s,bin); ref(s,s,'bin_id',bin.id); s.temp_bin_name=bin.name; },
'update-search-term': (s, search_term) => { s.search_term=search_term; },
'update-search-results': (s, _notes) => { s.notes =_notes.map(n=>({is_editing: false, temp_text: '', bin_id: s.bin.id, note:n})); },
'add-note': (s, {id, date}) => { s.notes.unshift({is_editing: true, temp_text: '', bin_id: s.bin.id, is_focused:true, note: {id: id, text: '', modified: date}}); },
'notes-loaded': (s, _notes) => { s.notes = _notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: s.bin.id, note:n})); },
'update-note-text': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; }, // updates underlying note text (i.e. the "model", not the app note_state) "in the background" (e.g. from a server-pushed update), regardless of whether it's being edited; "save" is a separate action, below
'update-note-editing': (s, {id, is_editing}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.is_editing=is_editing; note_s.temp_text=note_s.note.text; },
'save-note-edit': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note.id===id); note_s.note.text=text; note_s.temp_text=text; note_s.is_editing=false; },
'update-search-results': (s, _notes) => { integrateAll(s,s.notes,'note_id',_notes); s.notes =_notes.map(n=>({is_editing: false, temp_text: '', bin_id: s.bin_id, note_id:n.id})); },
'add-note': (s, {id, date}) => { i(s, {id: id, text: '', modified: date}); s.notes.unshift({is_editing: true, temp_text: '', bin_id: s.bin_id, is_focused:true, note_id: id}); },
'notes-loaded': (s, _notes) => { integrateAll(s,_notes); s.notes = _notes.map(n=>({is_editing: false, temp_text: n.text, bin_id: s.bin_id, note_id:n.id})); },
'update-note-text': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note_id===id); s.db[note_s.note_id].model.text=text; }, // updates underlying note text (i.e. the "model", not the app note_state) "in the background" (e.g. from a server-pushed update), regardless of whether it's being edited; "save" is a separate action, below
'update-note-editing': (s, {id, is_editing}) => { const note_s=s.notes.find(n=>n.note_id===id); note_s.is_editing=is_editing; note_s.temp_text=s.db[note_s.note_id].model.text; },
'save-note-edit': (s, {id, text}) => { const note_s=s.notes.find(n=>n.note_id===id); s.db[note_s.note_id].model.text=text; note_s.temp_text=text; note_s.is_editing=false; },
'update-sorting': (s, sorting) => { s.sorting=sorting; },
'update-username': (s, username) => { s.login.username=username; },
'update-password': (s, password) => { s.login.password=password; },
'login-requested': (s) => { s.login.showing=false; },
'login-succeeded': (s, {user, session_id}) => { s.login.is_logged_in=true; s.login.showing=true; s.login.password=''; s.login.user=user; s.login.session_id=session_id; },
'login-succeeded': (s, {user, session_id}) => { i(s, user); s.login.is_logged_in=true; s.login.showing=true; s.login.password=''; ref(s, s.login, 'user_id', user.id); s.login.session_id=session_id; },
'login-failed': (s) => { s.login.showing=true; s.login.password=''; },
'logout-requested': (s) => { s.login.is_logged_in=false; s.login.showing=true; s.login.username=''; s.login.password=''; s.login.session_id=''; s.login.bins=[]; },
'user-bin-list-loaded': (s, bins) => { s.login.bins=bins; },
'logout-requested': (s) => { s.login.is_logged_in=false; s.login.showing=true; s.login.username=''; s.login.password=''; s.login.session_id=''; refAll(s, s.login, 'bin_ids', []); },
'user-bin-list-loaded': (s, bins) => { s.login.bins=bins; integrateAll(s, bins); refAll(s, s.login, 'bin_ids', bins.map(b=>b.id)); },
'update-bin-name-editing': (s, is_editing) => { s.is_editing_bin_name=is_editing; },
'update-bin-name': (s, name) => { s.temp_bin_name=name; },
'commit-bin-name': (s) => { s.bin.name=s.temp_bin_name; s.is_editing_bin_name=false; addToBinListIfLoggedIn(s); }
'commit-bin-name': (s) => { s.db[s.bin_id].model.name=s.temp_bin_name; s.is_editing_bin_name=false; addToBinListIfLoggedIn(s); }
}, {
bin: {id: bin_id, name: bin_id, user_id: ''},
bin_id: bin_id,
is_editing_bin_name: false,
temp_bin_name: bin_id,
notes: [
//{is_editing: false, temp_text: '', bin_id: '', note: {id: nanoid(), text: 'Note one', modified: 1}},
//{is_editing: false, temp_text: '', bin_id: '', note_id: ''},
],
search_term: '',
sorting: 'new->old',
@ -71,12 +126,18 @@ const reducer = handleActions({
username: '', // value of textbox
password: '', // value of textbox
is_logged_in: false,
user: null /*{
user_id: '' /*{
id: '',
username: ''
}*/,
session_id: '',
bins: []
bin_ids: []
},
db: {
[bin_id]: {
ref_count: 1,
model: {id: bin_id, name: bin_id, user_id: ''}
}
}
//search_result_notes: []
});

Loading…
Cancel
Save