diff --git a/src/common_functions.js b/src/common_functions.js index 736202f6..e580ef7d 100644 --- a/src/common_functions.js +++ b/src/common_functions.js @@ -29,6 +29,11 @@ export function b64ToUni(bytes) { return new TextDecoder().decode(base64ToBytes(bytes)) } +export function areStringsEqual(str1, str2) { + const retVal = str1.localeCompare(str2) + return retVal === 0 +} + //////////////// expand, collapse and show or hide the children of the node //////////// export function showNode(node) { if (node) { @@ -185,4 +190,4 @@ export function isValidEmail(email) { return re.test(email) } -export default { uniTob64, b64ToUni, expandNode, collapseNode, showNode, hideNode, addToArray, createId, createLoadEventText, dedup, getLocationInfo, getSprintById, getSprintNameById, localTimeAndMilis, removeFromArray, isValidEmail } +export default { uniTob64, b64ToUni, areStringsEqual, expandNode, collapseNode, showNode, hideNode, addToArray, createId, createLoadEventText, dedup, getLocationInfo, getSprintById, getSprintNameById, localTimeAndMilis, removeFromArray, isValidEmail } diff --git a/src/components/admin/admin.js b/src/components/admin/admin.js index 16bd8498..894317b3 100644 --- a/src/components/admin/admin.js +++ b/src/components/admin/admin.js @@ -1,5 +1,5 @@ import { STATE, LEVEL } from '../../constants.js' -import { createId } from '../../common_functions.js' +import { uniTob64, createId } from '../../common_functions.js' import common_admin from './common_admin' import store from '../../store/store.js' @@ -81,8 +81,8 @@ const methods = { reqarea: null, title: this.productTitle, followers: [], - description: window.btoa(''), - acceptanceCriteria: window.btoa('

Please do not neglect

'), + description: uniTob64('


'), + acceptanceCriteria: uniTob64('

Please do not neglect

'), priority, comments: [{ ignoreEvent: 'comments initiated', diff --git a/src/components/views/coarse_product/c_product_view.js b/src/components/views/coarse_product/c_product_view.js index 78d2c0f1..be6a952a 100644 --- a/src/components/views/coarse_product/c_product_view.js +++ b/src/components/views/coarse_product/c_product_view.js @@ -103,25 +103,24 @@ const methods = { /* Event handling */ onNodesSelected(fromContextMenu) { - // update explicitly as the tree is not receiving focus due to the "user-select: none" css setting causing that @blur on the editor is not emitted - if (this.isDescriptionEdited) this.updateDescription(this.getPreviousNodeSelected) - if (this.isAcceptanceEdited) this.updateAcceptance(this.getPreviousNodeSelected) - // load the document - store.dispatch('loadDoc', { - id: this.getLastSelectedNode._id, - onSuccessCallback: () => { - // preset the req area color if available - this.selReqAreaColor = this.getLastSelectedNode.data.reqAreaItemColor - // if the user clicked on a node of another product (not root) - if (this.getLastSelectedNode._id !== 'root' && store.state.currentProductId !== this.getLastSelectedNode.productId) { - // update current productId and title - store.commit('switchCurrentProduct', this.getLastSelectedNode.productId) - } - if (this.getLastSelectedNode._id !== 'requirement-areas') { - if (!fromContextMenu) this.showSelectionEvent(store.state.selectedNodes) - } else this.showLastEvent('Create / maintain Requirement Areas here', SEV.INFO) + const onSuccessCallback = () => { + // preset the req area color if available + this.selReqAreaColor = this.getLastSelectedNode.data.reqAreaItemColor + // if the user clicked on a node of another product (not root) + if (this.getLastSelectedNode._id !== 'root' && store.state.currentProductId !== this.getLastSelectedNode.productId) { + // update current productId and title + store.commit('switchCurrentProduct', this.getLastSelectedNode.productId) } - }) + if (this.getLastSelectedNode._id !== 'requirement-areas') { + if (!fromContextMenu) this.showSelectionEvent(store.state.selectedNodes) + } else this.showLastEvent('Create / maintain Requirement Areas here', SEV.INFO) + } + + // update explicitly as the tree is not receiving focus due to the "user-select: none" css setting causing that @blur on the editor is not emitted + if (this.isDescriptionEdited) { this.updateDescription({ node: this.getPreviousNodeSelected, cb: onSuccessCallback }) } else + if (this.isAcceptanceEdited) { this.updateAcceptance({ node: this.getPreviousNodeSelected, cb: onSuccessCallback }) } else + // load the selected document + store.dispatch('loadDoc', { id: this.getLastSelectedNode._id, onSuccessCallback }) }, /* Use this event to check if the drag is allowed. If not, issue a warning */ diff --git a/src/components/views/coarse_product/c_product_view.vue b/src/components/views/coarse_product/c_product_view.vue index 2d77817d..5f9c31ee 100644 --- a/src/components/views/coarse_product/c_product_view.vue +++ b/src/components/views/coarse_product/c_product_view.vue @@ -60,7 +60,7 @@

State:

-

+

{{ getItemStateText(STATE.NEW) }} {{ getItemStateText(STATE.READY) }} @@ -204,7 +204,7 @@
- +
@@ -230,8 +230,8 @@
Filter {{ store.state.selectedForView }} - Clear {{ store.state.selectedForView }} filter - Add attachments + Clear {{ store.state.selectedForView }} filter + Add attachments
diff --git a/src/components/views/common_context.js b/src/components/views/common_context.js index 5a0b46c9..ebe48719 100644 --- a/src/components/views/common_context.js +++ b/src/components/views/common_context.js @@ -267,7 +267,7 @@ const methods = { conditionalFor: [], title: newNode.title, followers: newNode.data.followers, - description: uniTob64(''), + description: uniTob64('


'), acceptanceCriteria: newNode.level < this.TASKLEVEL ? uniTob64('

Please do not neglect

') : uniTob64('

See the acceptance criteria of the story/spike/defect.

'), priority: newNode.data.priority, comments: [{ diff --git a/src/components/views/common_listings.vue b/src/components/views/common_listings.vue index 57e36b7f..5d002880 100644 --- a/src/components/views/common_listings.vue +++ b/src/components/views/common_listings.vue @@ -91,8 +91,8 @@ function data() { editMyComment: false, editMyHistComment: false, commentObjToBeReplaced: {}, - myLastCommentText: "

", - myLastHistCommentText: "

" + myLastCommentText: "


", + myLastHistCommentText: "


" } } diff --git a/src/components/views/common_view.js b/src/components/views/common_view.js index d97fe70c..b8267b21 100644 --- a/src/components/views/common_view.js +++ b/src/components/views/common_view.js @@ -1,5 +1,5 @@ import { SEV, LEVEL, STATE } from '../../constants.js' -import { collapseNode } from '../../common_functions.js' +import { areStringsEqual } from '../../common_functions.js' import { constants, authorization, utilities } from '../mixins/generic.js' import store from '../../store/store.js' @@ -14,10 +14,8 @@ function data() { spikeSubtype: 1, defectSubtype: 2, docToUpdate: null, - newDescription: "

", - newAcceptance: "

", - isDescriptionEdited: false, - isAcceptanceEdited: false, + newDescription: "


", + newAcceptance: "


", editorToolbar: [ [{ header: [false, 1, 2, 3, 4, 5, 6] }], ['bold', 'italic', 'underline', 'strike'], @@ -30,11 +28,13 @@ function data() { // comments, history and attachments doAddition: false, startFiltering: false, + isDescriptionEdited: false, + isAcceptanceEdited: false, isCommentsFilterActive: false, isHistoryFilterActive: false, - newComment: "

", + newComment: "


", fileInfo: null, - newHistory: "

", + newHistory: "


", filterForCommentPrep: '', filterForHistoryPrep: '', // move data @@ -143,7 +143,7 @@ const computed = { let retVal = store.state.currentDoc.description if (retVal === "") { // correct empty field - retVal = "

" + retVal = "


" } return retVal }, @@ -157,7 +157,7 @@ const computed = { let retVal = store.state.currentDoc.acceptanceCriteria if (retVal === "") { // correct empty field - retVal = "

" + retVal = "


" } return retVal }, @@ -182,14 +182,14 @@ const watch = { } if (store.state.selectedForView === 'comments') { if (this.canCreateComments) { - this.newComment = "

" + this.newComment = "


" this.$refs.commentsEditorRef.show() } else { this.showLastEvent('Sorry, your assigned role(s) disallow you to create comments', SEV.WARNING) } } if (store.state.selectedForView === 'history') { - this.newHistory = "

" + this.newHistory = "


" this.$refs.historyEditorRef.show() } } @@ -552,32 +552,46 @@ const methods = { }, /* Tree and database update methods */ - updateDescription(node = this.getLastSelectedNode) { - if (this.isDescriptionEdited && (store.state.currentDoc.description !== this.newDescription)) { - // update skipped when not changed + updateDescription(payload) { + // node is either the current node (descripton changed and a click outside description and not on a node) or + // the previous selected node (description changed and clicked on a node) + const node = payload.node + if (areStringsEqual(store.state.currentDoc.description, this.newDescription)) { + // update skipped when not changed; load the doc of last clicked node this.isDescriptionEdited = false + store.dispatch('loadDoc', { id: this.getLastSelectedNode._id, onSuccessCallback: payload.cb }) + } else { + const toDispatch = [{ loadDoc: { id: this.getLastSelectedNode._id, onSuccessCallback: payload.cb } }] if (this.haveAccessInTree(node.productId, this.getCurrentItemLevel, store.state.currentDoc.team, 'change the description of this item')) { store.dispatch('saveDescription', { node, newDescription: this.newDescription, - timestamp: Date.now() + timestamp: Date.now(), + toDispatch }) - } + } else this.isDescriptionEdited = false } }, - updateAcceptance(node = this.getLastSelectedNode) { + updateAcceptance(payload) { + // node is either the current node (descripton changed and a click outside description and not on a node) or + // the previous selected node (description changed and clicked on a node) + const node = payload.node if (node._id !== 'requirement-areas' && node.parentId !== 'requirement-areas') { - if (this.isAcceptanceEdited && (store.state.currentDoc.acceptanceCriteria !== this.newAcceptance)) { - // update skipped when not changed + if (areStringsEqual(store.state.currentDoc.acceptanceCriteria, this.newAcceptance)) { + // update skipped when not changed; load the doc of last clicked node this.isAcceptanceEdited = false + store.dispatch('loadDoc', { id: this.getLastSelectedNode._id, onSuccessCallback: payload.cb }) + } else { + const toDispatch = [{ loadDoc: { id: this.getLastSelectedNode._id, onSuccessCallback: payload.cb } }] if (this.haveAccessInTree(node.productId, this.getCurrentItemLevel, store.state.currentDoc.team, 'change the acceptance criteria of this item')) { store.dispatch('saveAcceptance', { node, newAcceptance: this.newAcceptance, - timestamp: Date.now() + timestamp: Date.now(), + toDispatch }) - } + } else this.isAcceptanceEdited = false } } }, diff --git a/src/components/views/detail_product/d_product_view.js b/src/components/views/detail_product/d_product_view.js index 3243e256..4c6714a3 100644 --- a/src/components/views/detail_product/d_product_view.js +++ b/src/components/views/detail_product/d_product_view.js @@ -114,7 +114,9 @@ const methods = { /* event handling */ onNodesSelected(fromContextMenu) { - const afterNewItemLoad = () => { + const onSuccessCallback = () => { + this.isDescriptionEdited = false + this.isAcceptanceEdited = false // if the user clicked on a node of another product (not root) const currentProductId = store.state.currentProductId if (this.getLastSelectedNode._id !== 'root' && currentProductId !== this.getLastSelectedNode.productId) { @@ -125,51 +127,12 @@ const methods = { } if (!fromContextMenu) this.showSelectionEvent(store.state.selectedNodes) } - const toDispatch = [{ - loadDoc: { - id: this.getLastSelectedNode._id, - onSuccessCallback: afterNewItemLoad() - } - }] - // update explicitly as the tree is not receiving focus due to the "user-select: none" css setting causing that @blur on the editor is not emitted immediately - if (this.isDescriptionEdited) { - this.isDescriptionEdited = false - const node = this.getPreviousNodeSelected - if (store.state.currentDoc.description !== this.newDescription) { - // update skipped when not changed - if (this.haveAccessInTree(node.productId, this.getCurrentItemLevel, store.state.currentDoc.team, 'change the description of this item')) { - store.dispatch('saveDescription', { - node, - newDescription: this.newDescription, - timestamp: Date.now(), - toDispatch - }) - } - } - } else if (this.isAcceptanceEdited) { - this.isAcceptanceEdited = false - const node = this.getPreviousNodeSelected - if (node._id !== 'requirement-areas' && node.parentId !== 'requirement-areas') { - // update skipped when not changed - if (store.state.currentDoc.acceptanceCriteria !== this.newAcceptance) { - if (this.haveAccessInTree(node.productId, this.getCurrentItemLevel, store.state.currentDoc.team, 'change the acceptance criteria of this item')) { - store.dispatch('saveAcceptance', { - node, - newAcceptance: this.newAcceptance, - timestamp: Date.now(), - toDispatch - }) - } - } - } - } else { - // just load the selected document - store.dispatch('loadDoc', { - id: this.getLastSelectedNode._id, - onSuccessCallback: afterNewItemLoad() - }) - } + // update explicitly as the tree is not receiving focus due to the "user-select: none" css setting causing that @blur on the editor is not emitted + if (this.isDescriptionEdited) { this.updateDescription({ node: this.getPreviousNodeSelected, cb: onSuccessCallback }) } else + if (this.isAcceptanceEdited) { this.updateAcceptance({ node: this.getPreviousNodeSelected, cb: onSuccessCallback }) } else + // load the selected document + store.dispatch('loadDoc', { id: this.getLastSelectedNode._id, onSuccessCallback }) }, /* Use this event to check if the drag is allowed. If not, issue a warning */ diff --git a/src/components/views/detail_product/d_product_view.vue b/src/components/views/detail_product/d_product_view.vue index 33f9825b..c7b407c5 100644 --- a/src/components/views/detail_product/d_product_view.vue +++ b/src/components/views/detail_product/d_product_view.vue @@ -229,7 +229,8 @@
- + +
@@ -238,7 +239,7 @@
- +
diff --git a/src/store/modules/initdb.js b/src/store/modules/initdb.js index 48e594dc..6cc1c482 100644 --- a/src/store/modules/initdb.js +++ b/src/store/modules/initdb.js @@ -1,5 +1,5 @@ import { LEVEL, MISC } from '../../constants.js' -import { createId } from '../../common_functions.js' +import { uniTob64, createId } from '../../common_functions.js' import globalAxios from 'axios' // IMPORTANT: all updates on the backlogitem documents must add history in order for the changes feed to work properly (if omitted the previous event will be processed again) @@ -499,8 +499,8 @@ const actions = { title: 'The root of all products in this database', team: 'n/a', followers: [], - description: window.btoa('

Database root document

'), - acceptanceCriteria: window.btoa('

not applicable

'), + description: uniTob64('

Database root document

'), + acceptanceCriteria: uniTob64('

not applicable

'), priority: 0, comments: [{ ignoreEvent: 'comments initiated', @@ -579,8 +579,8 @@ const actions = { spikepersonhours: 0, title: 'REQUIREMENT AREAS', followers: [], - description: window.btoa('

To insert one or more requirement areas inside this node right-click on this nodes title in the tree view.

'), - acceptanceCriteria: window.btoa('

n/a

'), + description: uniTob64('

To insert one or more requirement areas inside this node right-click on this nodes title in the tree view.

'), + acceptanceCriteria: uniTob64('

n/a

'), // do not set a priority, must be null comments: [{ ignoreEvent: 'comments initiated', @@ -625,8 +625,8 @@ const actions = { reqarea: null, title, followers: [], - description: window.btoa(''), - acceptanceCriteria: window.btoa('

Please do not neglect

'), + description: uniTob64('


'), + acceptanceCriteria: uniTob64('

Please do not neglect

'), priority: 0, comments: [{ ignoreEvent: 'comments initiated', diff --git a/src/store/modules/planningboard.js b/src/store/modules/planningboard.js index 4df6a77b..e0235d0a 100644 --- a/src/store/modules/planningboard.js +++ b/src/store/modules/planningboard.js @@ -1,5 +1,5 @@ import { SEV, LEVEL, STATE } from '../../constants.js' -import { expandNode, getSprintNameById } from '../../common_functions.js' +import { expandNode, getSprintNameById, uniTob64 } from '../../common_functions.js' import globalAxios from 'axios' // IMPORTANT: all updates on the backlogitem documents must add history in order for the changes feed to work properly (if omitted the previous event will be processed again) // Save the history, to trigger the distribution to other online users, when all other database updates are done. @@ -1011,8 +1011,8 @@ const actions = { conditionalFor: [], title: payload.taskTitle, followers: storyDoc.followers || [], - description: window.btoa(''), - acceptanceCriteria: window.btoa('

See the acceptance criteria of the story/spike/defect.

'), + description: uniTob64('


'), + acceptanceCriteria: uniTob64('

See the acceptance criteria of the story/spike/defect.

'), priority: taskPriority, comments: [{ ignoreEvent: 'comments initiated', diff --git a/src/store/modules/update.js b/src/store/modules/update.js index 9e526cd7..a89f77d7 100644 --- a/src/store/modules/update.js +++ b/src/store/modules/update.js @@ -735,32 +735,37 @@ const actions = { const prevLastContentChange = tmpDoc.lastContentChange || 0 tmpDoc.lastContentChange = payload.timestamp tmpDoc.lastChange = payload.timestamp - tmpDoc.description = newEncodedDescription + + const onSuccessCallback = () => { + rootState.isDescriptionEdited = false + commit('updateNodesAndCurrentDoc', { node, description: payload.newDescription, lastContentChange: payload.timestamp, newHist }) + if (!payload.isUndoAction || payload.isUndoAction === undefined) { + commit('addToEventList', { txt: `The description of item with short id ${id.slice(-5)} is changed`, severity: SEV.INFO }) + // create an entry for undoing the change in a last-in first-out sequence + const entry = { + node, + type: 'undoDescriptionChange', + oldDescription, + prevLastContentChange + } + rootState.changeHistory.unshift(entry) + } else { + commit('addToEventList', { txt: 'Change of the item description is undone', severity: SEV.INFO }) + rootState.busyWithLastUndo = false + } + } + + const onFailureCallback = () => { + if (payload.isUndoAction) rootState.busyWithLastUndo = false + } + dispatch('updateDoc', { dbName: rootState.userData.currentDb, updatedDoc: tmpDoc, caller: 'saveDescription', - onSuccessCallback: () => { - commit('updateNodesAndCurrentDoc', { node, description: payload.newDescription, lastContentChange: payload.timestamp, newHist }) - if (!payload.isUndoAction || payload.isUndoAction === undefined) { - commit('addToEventList', { txt: `The description of item with short id ${id.slice(-5)} is changed`, severity: SEV.INFO }) - // create an entry for undoing the change in a last-in first-out sequence - const entry = { - node, - type: 'undoDescriptionChange', - oldDescription, - prevLastContentChange - } - rootState.changeHistory.unshift(entry) - } else { - commit('addToEventList', { txt: 'Change of the item description is undone', severity: SEV.INFO }) - rootState.busyWithLastUndo = false - } - }, - onFailureCallback: () => { - if (payload.isUndoAction) rootState.busyWithLastUndo = false - }, + onSuccessCallback, + onFailureCallback, toDispatch: payload.toDispatch }) }).catch(error => { @@ -799,32 +804,37 @@ const actions = { const prevLastContentChange = tmpDoc.lastContentChange || 0 tmpDoc.lastContentChange = payload.timestamp tmpDoc.lastChange = payload.timestamp - tmpDoc.acceptanceCriteria = newEncodedAcceptance + + const onSuccessCallback = () => { + rootState.isAcceptanceEdited = false + commit('updateNodesAndCurrentDoc', { node, acceptanceCriteria: payload.newAcceptance, lastContentChange: payload.timestamp, newHist }) + if (!payload.isUndoAction || payload.isUndoAction === undefined) { + commit('addToEventList', { txt: `The acceptance criteria of item with short id ${id.slice(-5)} are changed`, severity: SEV.INFO }) + // create an entry for undoing the change in a last-in first-out sequence + const entry = { + node, + type: 'undoAcceptanceChange', + oldAcceptance, + prevLastContentChange + } + rootState.changeHistory.unshift(entry) + } else { + commit('addToEventList', { txt: 'Change of the item acceptance criteria is undone', severity: SEV.INFO }) + rootState.busyWithLastUndo = false + } + } + + const onFailureCallback = () => { + if (payload.isUndoAction) rootState.busyWithLastUndo = false + } + dispatch('updateDoc', { dbName: rootState.userData.currentDb, updatedDoc: tmpDoc, caller: 'saveAcceptance', - onSuccessCallback: () => { - commit('updateNodesAndCurrentDoc', { node, acceptanceCriteria: payload.newAcceptance, lastContentChange: payload.timestamp, newHist }) - if (!payload.isUndoAction || payload.isUndoAction === undefined) { - commit('addToEventList', { txt: `The acceptance criteria of item with short id ${id.slice(-5)} are changed`, severity: SEV.INFO }) - // create an entry for undoing the change in a last-in first-out sequence - const entry = { - node, - type: 'undoAcceptanceChange', - oldAcceptance, - prevLastContentChange - } - rootState.changeHistory.unshift(entry) - } else { - commit('addToEventList', { txt: 'Change of the item acceptance criteria is undone', severity: SEV.INFO }) - rootState.busyWithLastUndo = false - } - }, - onFailureCallback: () => { - if (payload.isUndoAction) rootState.busyWithLastUndo = false - }, + onSuccessCallback, + onFailureCallback, toDispatch: payload.toDispatch }) }).catch(error => { @@ -1076,6 +1086,7 @@ const actions = { /* * Create or update an existing document by creating a new revision. + * Must call loadDoc on success to update the current doc visable to the user. * Executes a onSuccessCallback and onFailureCallback if provided in the payload. */ updateDoc({