mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 23:40:13 +01:00
Replace the component bounded cachedValue by a global UnsavedEdits
This new draft saving system is currently only implemented for the card description and comment. We need better a component inheritance/composition model to support this for all editable fields. Fixes #186
This commit is contained in:
parent
cc88e78483
commit
d644cba38f
13 changed files with 252 additions and 95 deletions
|
|
@ -17,10 +17,10 @@ EscapeActions = {
|
|||
'inlinedForm',
|
||||
'detailsPane',
|
||||
'multiselection',
|
||||
'sidebarView'
|
||||
'sidebarView',
|
||||
],
|
||||
|
||||
register: function(label, action, condition = () => true, options = {}) {
|
||||
register(label, action, condition = () => true, options = {}) {
|
||||
const priority = this.hierarchy.indexOf(label);
|
||||
if (priority === -1) {
|
||||
throw Error('You must define the label in the EscapeActions hierarchy');
|
||||
|
|
@ -33,35 +33,35 @@ EscapeActions = {
|
|||
|
||||
let noClickEscapeOn = options.noClickEscapeOn;
|
||||
|
||||
this._actions[priority] = {
|
||||
this._actions = _.sortBy([...this._actions, {
|
||||
priority,
|
||||
condition,
|
||||
action,
|
||||
noClickEscapeOn,
|
||||
enabledOnClick
|
||||
};
|
||||
enabledOnClick,
|
||||
}], (action) => action.priority);
|
||||
},
|
||||
|
||||
executeLowest: function() {
|
||||
executeLowest() {
|
||||
return this._execute({
|
||||
multipleAction: false
|
||||
});
|
||||
},
|
||||
|
||||
executeAll: function() {
|
||||
executeAll() {
|
||||
return this._execute({
|
||||
multipleActions: true
|
||||
});
|
||||
},
|
||||
|
||||
executeUpTo: function(maxLabel) {
|
||||
executeUpTo(maxLabel) {
|
||||
return this._execute({
|
||||
maxLabel: maxLabel,
|
||||
multipleActions: true
|
||||
});
|
||||
},
|
||||
|
||||
clickExecute: function(target, maxLabel) {
|
||||
clickExecute(target, maxLabel) {
|
||||
if (this._nextclickPrevented) {
|
||||
this._nextclickPrevented = false;
|
||||
} else {
|
||||
|
|
@ -74,18 +74,18 @@ EscapeActions = {
|
|||
}
|
||||
},
|
||||
|
||||
preventNextClick: function() {
|
||||
preventNextClick() {
|
||||
this._nextclickPrevented = true;
|
||||
},
|
||||
|
||||
_stopClick: function(action, clickTarget) {
|
||||
_stopClick(action, clickTarget) {
|
||||
if (! _.isString(action.noClickEscapeOn))
|
||||
return false;
|
||||
else
|
||||
return $(clickTarget).closest(action.noClickEscapeOn).length > 0;
|
||||
},
|
||||
|
||||
_execute: function(options) {
|
||||
_execute(options) {
|
||||
const maxLabel = options.maxLabel;
|
||||
const multipleActions = options.multipleActions;
|
||||
const isClick = !! options.isClick;
|
||||
|
|
@ -99,8 +99,7 @@ EscapeActions = {
|
|||
else
|
||||
maxPriority = this.hierarchy.indexOf(maxLabel);
|
||||
|
||||
for (let i = 0; i < this._actions.length; i++) {
|
||||
let currentAction = this._actions[i];
|
||||
for (let currentAction of this._actions) {
|
||||
if (currentAction.priority > maxPriority)
|
||||
return executedAtLeastOne;
|
||||
|
||||
|
|
|
|||
78
client/lib/inlinedform.js
Normal file
78
client/lib/inlinedform.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// A inlined form is used to provide a quick edition of single field for a given
|
||||
// document. Clicking on a edit button should display the form to edit the field
|
||||
// value. The form can then be submited, or just closed.
|
||||
//
|
||||
// When the form is closed we save non-submitted values in memory to avoid any
|
||||
// data loss.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// +inlineForm
|
||||
// // the content when the form is open
|
||||
// else
|
||||
// // the content when the form is close (optional)
|
||||
|
||||
// We can only have one inlined form element opened at a time
|
||||
currentlyOpenedForm = new ReactiveVar(null);
|
||||
|
||||
InlinedForm = BlazeComponent.extendComponent({
|
||||
template: function() {
|
||||
return 'inlinedForm';
|
||||
},
|
||||
|
||||
onCreated: function() {
|
||||
this.isOpen = new ReactiveVar(false);
|
||||
},
|
||||
|
||||
onDestroyed: function() {
|
||||
currentlyOpenedForm.set(null);
|
||||
},
|
||||
|
||||
open: function() {
|
||||
// Close currently opened form, if any
|
||||
EscapeActions.executeUpTo('inlinedForm');
|
||||
this.isOpen.set(true);
|
||||
currentlyOpenedForm.set(this);
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.isOpen.set(false);
|
||||
currentlyOpenedForm.set(null);
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var input = this.find('textarea,input[type=text]');
|
||||
return this.isOpen.get() && input && input.value;
|
||||
},
|
||||
|
||||
events: function() {
|
||||
return [{
|
||||
'click .js-close-inlined-form': this.close,
|
||||
'click .js-open-inlined-form': this.open,
|
||||
|
||||
// Pressing Ctrl+Enter should submit the form
|
||||
'keydown form textarea': function(evt) {
|
||||
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
|
||||
this.find('button[type=submit]').click();
|
||||
}
|
||||
},
|
||||
|
||||
// Close the inlined form when after its submission
|
||||
submit: function() {
|
||||
if (this.currentData().autoclose !== false) {
|
||||
Tracker.afterFlush(() => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
}).register('inlinedForm');
|
||||
|
||||
// Press escape to close the currently opened inlinedForm
|
||||
EscapeActions.register('inlinedForm',
|
||||
function() { currentlyOpenedForm.get().close(); },
|
||||
function() { return currentlyOpenedForm.get() !== null; }, {
|
||||
noClickEscapeOn: '.js-inlined-form'
|
||||
}
|
||||
);
|
||||
82
client/lib/unsavedEdits.js
Normal file
82
client/lib/unsavedEdits.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
Meteor.subscribe('unsaved-edits');
|
||||
|
||||
// `UnsavedEdits` is a global key-value store used to save drafts of user
|
||||
// inputs. We used to have the notion of a `cachedValue` that was local to a
|
||||
// component but the global store has multiple advantages:
|
||||
// 1. When the component is unmounted (ie, destroyed) the draft isn't lost
|
||||
// 2. The drafts are synced across multiple computers
|
||||
// 3. The drafts are synced across multiple browser tabs
|
||||
// XXX This currently doesn't work in purely offline mode since the sync is
|
||||
// handled with the DDP connection to the server. To solve this, we could use
|
||||
// something like GroundDB that syncs using localstorage.
|
||||
//
|
||||
// The key is a dictionary composed of two fields:
|
||||
// * a `fieldName` which identifies the particular field. Since this is a global
|
||||
// identifier a good practice would be to compose it with the collection name
|
||||
// and the document field, eg. `boardTitle`, `cardDescription`.
|
||||
// * a `docId` which identifies the appropriate document. In general we use
|
||||
// MongoDB `_id` field.
|
||||
//
|
||||
// The value is a string containing the draft.
|
||||
|
||||
UnsavedEdits = {
|
||||
// XXX Wanted to have the collection has an instance variable, but
|
||||
// unfortunately the collection isn't defined yet at this point. We need ES6
|
||||
// modules to solve the file order issue!
|
||||
//
|
||||
// _collection: UnsavedEditCollection,
|
||||
|
||||
get({ fieldName, docId }, defaultTo = '') {
|
||||
let unsavedValue = this._getCollectionDocument(fieldName, docId);
|
||||
if (unsavedValue) {
|
||||
return unsavedValue.value
|
||||
} else {
|
||||
return defaultTo;
|
||||
}
|
||||
},
|
||||
|
||||
has({ fieldName, docId }) {
|
||||
return Boolean(this.get({fieldName, docId}));
|
||||
},
|
||||
|
||||
set({ fieldName, docId }, value) {
|
||||
let currentDoc = this._getCollectionDocument(fieldName, docId);
|
||||
if (currentDoc) {
|
||||
UnsavedEditCollection.update(currentDoc._id, {
|
||||
$set: {
|
||||
value: value
|
||||
}
|
||||
});
|
||||
} else {
|
||||
UnsavedEditCollection.insert({
|
||||
fieldName,
|
||||
docId,
|
||||
value,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reset({ fieldName, docId }) {
|
||||
let currentDoc = this._getCollectionDocument(fieldName, docId);
|
||||
if (currentDoc) {
|
||||
UnsavedEditCollection.remove(currentDoc._id);
|
||||
}
|
||||
},
|
||||
|
||||
_getCollectionDocument(fieldName, docId) {
|
||||
return UnsavedEditCollection.findOne({fieldName, docId});
|
||||
}
|
||||
}
|
||||
|
||||
Blaze.registerHelper('getUnsavedValue', (fieldName, docId, defaultTo) => {
|
||||
// Workaround some blaze feature that ass a list of keywords arguments as the
|
||||
// last parameter (even if the caller didn't specify any).
|
||||
if (! _.isString(defaultTo)) {
|
||||
defaultTo = '';
|
||||
}
|
||||
return UnsavedEdits.get({ fieldName, docId }, defaultTo);
|
||||
});
|
||||
|
||||
Blaze.registerHelper('hasUnsavedValue', (fieldName, docId) => {
|
||||
return UnsavedEdits.has({ fieldName, docId });
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue