diff --git a/_ur_addons/comment/ac-comment.ts b/_ur_addons/comment/ac-comment.ts index 3ea3546f2..641d3c870 100644 --- a/_ur_addons/comment/ac-comment.ts +++ b/_ur_addons/comment/ac-comment.ts @@ -8,34 +8,35 @@ COMMENTCOLLECTION ccol ----------------- - A COMMENTCOLLECTION is the main data source for the CommentBtn. + A COMENTCOLLECTION is the main data source for the CommentBtn. It primarily shows summary information for the three states of the button: * has no comments * has unread comments * has read comments + * number of non-deleted comments in the collection It passes on the collection_ref to the CommentThread components. interface CommentCollection { cref: any; // collection_ref hasUnreadComments: boolean; hasReadComments: boolean; + commentCount: number; } COMMENTUISTATE cui -------------- - COMMENTUISTATE keeps track of the `isOpen` state of currently selected/open - comment ui buttons. - Comments can be opened and closed from multiple UI elements. + A COMMENTUISTATE object can be opened and closed from multiple UI elements. + COMMENTUI keeps track of the `isOpen` status based on the UI element. e.g. a comment button in a node can open a comment but the same comment can - be opened from the node table view. + be opeend from the node table view. COMMENTUISTATE Map OPENCOMMENTS - --------------- - OPENCOMMENTS keeps track of currently open comments. This is + ------------ + OPENCOMMENTS keeps track of currently open comment buttons. This is used prevent two comment buttons from opening the same comment collection, e.g. if the user opens a node and a node table comment at the same time. @@ -68,8 +69,6 @@ isMarkedRead: boolean; isReplyToMe: boolean; allowReply: boolean; - - markedRead: boolean; } \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * /////////////////////////////////////*/ @@ -96,6 +95,7 @@ type TCommentCollection = { collection_ref: TCollectionRef; hasUnreadComments?: boolean; hasReadComments?: boolean; + commentCount?: number; }; type TCommentUIRef = string; // comment button id, e.g. `n32-isTable`, not just TCollectionRef @@ -113,9 +113,8 @@ type TCommentVisualObject = { isBeingEdited: boolean; isEditable: boolean; isMarkedRead: boolean; - isReplyToMe: boolean; - allowReply: boolean; - markedRead: boolean; + isReplyToMe?: boolean; + allowReply?: boolean; }; type TCommentCollectionMap = Map; @@ -154,6 +153,12 @@ function LoadTemplate(commentTypes: Array) { * @param {any} data JSON data */ function LoadDB(data) { + COMMENTCOLLECTION.clear(); + COMMENTUISTATE.clear(); + OPENCOMMENTS.clear(); + COMMENTS_BEING_EDITED.clear(); + COMMENTVOBJS.clear(); + if (DBG) console.log(PR, 'LoadDB', data); DCCOMMENTS.LoadDB(data); if (DBG) console.log('COMMENTCOLLECTION', COMMENTCOLLECTION); @@ -203,6 +208,23 @@ function CloseCommentCollection( DeriveThreadedViewObjects(cref, uid); } +/** + * Close comment collections WITHOUT marking them read + * Used by comment status when user is quickly opening + * comments for review + */ +function CloseAllCommentCollections(uid: TUserID) { + COMMENTUISTATE.forEach((state, uiref) => { + if (state.isOpen) { + // Set isOpen status + COMMENTUISTATE.set(uiref, { cref: state.cref, isOpen: false }); + OPENCOMMENTS.set(state.cref, undefined); + // Update Derived Lists to update Marked status + DeriveThreadedViewObjects(state.cref, uid); + } + }); +} + function MarkRead(cref: TCollectionRef, uid: TUserID) { // Mark Read const commentVObjs = COMMENTVOBJS.get(cref); @@ -232,14 +254,16 @@ function GetCommentStats(uid: TUserID): { COMMENTVOBJS.forEach(cvobjs => { cvobjs.forEach(cvobj => { if (!cvobj.isMarkedRead) { - // count unread - countUnread++; - // count repliesToMe const comment = DCCOMMENTS.GetComment(cvobj.comment_id); - if (rootCidsWithRepliesToMe.includes(comment.comment_id_parent)) { - // HACK: Update cvobj by reference! - cvobj.isReplyToMe = true; - countRepliesToMe++; + if (!comment.comment_isMarkedDeleted) { + // count unread + countUnread++; + // count repliesToMe + if (rootCidsWithRepliesToMe.includes(comment.comment_id_parent)) { + // HACK: Update cvobj by reference! + cvobj.isReplyToMe = true; + countRepliesToMe++; + } } } }); @@ -265,16 +289,23 @@ function GetOpenComments(cref: TCollectionRef): TCommentUIRef { /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// EDITABLE COMMENTS -function m_RegisterCommentBeingEdited(cid: TCommentID) { +function RegisterCommentBeingEdited(cid: TCommentID) { COMMENTS_BEING_EDITED.set(cid, cid); } -function m_DeRegisterCommentBeingEdited(cid: TCommentID) { +function DeRegisterCommentBeingEdited(cid: TCommentID) { COMMENTS_BEING_EDITED.delete(cid); } - function GetCommentBeingEdited(cid: TCommentID): TCommentID { return COMMENTS_BEING_EDITED.get(cid); } +/// ANY comment is being edited +function GetCommentsAreBeingEdited(): boolean { + return COMMENTS_BEING_EDITED.size > 0; +} + +function GetCommentsBeingEdited(): TCommentsBeingEditedMap { + return COMMENTS_BEING_EDITED; +} /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// UNREAD COMMENTS @@ -302,6 +333,8 @@ function GetUnreadComments(): TComment[] { /// COMMENT THREAD VIEW OBJECTS function DeriveAllThreadedViewObjects(uid: TUserID) { + COMMENTCOLLECTION.clear(); + COMMENTVOBJS.clear(); const crefs = DCCOMMENTS.GetCrefs(); crefs.forEach(cref => DeriveThreadedViewObjects(cref, uid)); } @@ -317,12 +350,14 @@ function DeriveThreadedViewObjects( ): TCommentVisualObject[] { if (cref === undefined) throw new Error(`m_DeriveThreadedViewObjects cref: "${cref}" must be defined!`); - const commentVObjs = []; + const commentVObjs: TCommentVisualObject[] = []; + let commentCount = 0; const threadIds = DCCOMMENTS.GetThreadedCommentIds(cref); threadIds.forEach(cid => { const comment = DCCOMMENTS.GetComment(cid); if (comment === undefined) console.error('GetThreadedViewObjects for cid not found', cid, 'in', threadIds); + if (!comment.comment_isMarkedDeleted) commentCount++; const level = comment.comment_id_parent === '' ? 0 : 1; commentVObjs.push({ comment_id: cid, @@ -367,6 +402,7 @@ function DeriveThreadedViewObjects( }); ccol.hasUnreadComments = hasUnreadComments; ccol.hasReadComments = hasReadComments; + ccol.commentCount = commentCount; COMMENTCOLLECTION.set(cref, ccol); return commentReplyVObjs; } @@ -385,21 +421,15 @@ function GetThreadedViewObjects( : commentVObjs; } -/** - * @param {string} cref - * @param {string} uid -- User ID is used to determine read/unread status - * @returns {number} Returns the number of comments in a collection - */ -function GetThreadedViewObjectsCount(cref: TCollectionRef, uid: TUserID): number { - return GetThreadedViewObjects(cref, uid).length; -} - function GetCOMMENTVOBJS(): TCommentVisualObjectsMap { return COMMENTVOBJS; } function GetCommentVObj(cref: TCollectionRef, cid: TCommentID): TCommentVisualObject { const thread = COMMENTVOBJS.get(cref); + if (thread === undefined) + // fail gracefully if thread not found -- this can happen if the comment has been deleted + return; const cvobj = thread.find(c => c.comment_id === cid); return cvobj; } @@ -428,7 +458,7 @@ function AddComment(data: { COMMENTVOBJS ); cvobj.isBeingEdited = true; - m_RegisterCommentBeingEdited(comment.comment_id); + RegisterCommentBeingEdited(comment.comment_id); commentVObjs = commentVObjs.map(c => c.comment_id === cvobj.comment_id ? cvobj : c @@ -454,7 +484,7 @@ function UpdateComment(cobj: TComment, uid: TUserID) { const cvobj = GetCommentVObj(cobj.collection_ref, cobj.comment_id); if (cvobj === undefined) throw new Error( - `ac-comment.UpdateComment could not find cobj ${cobj.comment_id}. Maybe it hasn't been created yet? ${COMMENTVOBJS}` + `ac-comment.UpdateComment could not find cvobj ${cobj.comment_id}. Maybe it hasn't been created yet? ${COMMENTVOBJS}` ); // mark it unread @@ -462,7 +492,7 @@ function UpdateComment(cobj: TComment, uid: TUserID) { DCCOMMENTS.MarkCommentUnread(cvobj.comment_id, uid); cvobj.isBeingEdited = false; - m_DeRegisterCommentBeingEdited(cobj.comment_id); + DeRegisterCommentBeingEdited(cobj.comment_id); cvobj.modifytime_string = GetDateString(cobj.comment_modifytime); commentVObjs = commentVObjs.map(c => c.comment_id === cvobj.comment_id ? cvobj : c @@ -523,8 +553,9 @@ function RemoveAllCommentsForCref(parms: { * Does NOT trigger a database update * (Contrast this with RemoveComment above) */ -function HandleRemovedComments(comment_ids: TCommentID[]) { +function HandleRemovedComments(comment_ids: TCommentID[], uid: TUserID) { DCCOMMENTS.HandleRemovedComments(comment_ids); + DeriveAllThreadedViewObjects(uid); } /// PASS-THROUGH METHODS ////////////////////////////////////////////////////// @@ -567,6 +598,7 @@ export { GetCommentCollection, UpdateCommentUIState, CloseCommentCollection, + CloseAllCommentCollections, MarkRead, GetCommentStats, // Comment UI State @@ -574,7 +606,11 @@ export { // Open Comments GetOpenComments, // Editable Comments + RegisterCommentBeingEdited, + DeRegisterCommentBeingEdited, GetCommentBeingEdited, + GetCommentsAreBeingEdited, + GetCommentsBeingEdited, // Unread Comments GetUnreadRepliesToMe, GetUnreadComments, @@ -582,7 +618,6 @@ export { DeriveAllThreadedViewObjects, DeriveThreadedViewObjects, GetThreadedViewObjects, - GetThreadedViewObjectsCount, GetCOMMENTVOBJS, GetCommentVObj, // Comment Objects diff --git a/_ur_addons/comment/dc-comment.ts b/_ur_addons/comment/dc-comment.ts index f73214424..a6c4283cd 100644 --- a/_ur_addons/comment/dc-comment.ts +++ b/_ur_addons/comment/dc-comment.ts @@ -3,7 +3,8 @@ dc-comments Data Care Comments - + Ported from Net.Create a5a947d5007bfef213bb107c521c59547fa72714 + DATA COMMENTS @@ -24,6 +25,8 @@ commenter_id: any; commenter_text: string[]; + + id: number; }; @@ -62,6 +65,10 @@ r2 "r3 Third Comment" r4 r4 "r4 Fourth Comment" + "thread" -- a sequence starting with the first + e.g. [r1, r2, r3, r4] + e.g. [r2.1, r2.2, r2.3] + \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * /////////////////////////////////////*/ const DBG = false; @@ -80,7 +87,16 @@ type TUserObject = { // REVIEW CType needs to be defined after we figure out how to handle templates // Eventually we will dynamically define them. // Comment Template Type Slug -export type CType = 'cmt' | 'tellmemore' | 'source' | 'demo'; +export type CType = + | 'cmt' + | 'tellmemore' + | 'source' + | 'evidence' + | 'clarity' + | 'steps' + | 'response' + | string; // allow other slugs + type CPromptFormat = | 'text' | 'dropdown' @@ -88,6 +104,8 @@ type CPromptFormat = | 'radio' | 'likert' | 'discrete-slider'; + +export type TDBRecordId = number; export type TCommentID = string; export type TCommentType = { slug: CType; @@ -99,16 +117,18 @@ type TCommentPrompt = { prompt: string; options?: string[]; help: string; - feedback: string; + feedback?: string; }; export type TCollectionRef = any; export type TComment = { + id?: TDBRecordId; // gets added by pmcData after it's added to the db + collection_ref: TCollectionRef; // aka 'cref' comment_id: TCommentID; comment_id_parent: any; comment_id_previous: any; - comment_type: string; + comment_type: CType; // slug comment_createtime: number; comment_modifytime: number; comment_isMarkedDeleted: boolean; @@ -132,9 +152,13 @@ type TLokiData = { }; export type TCommentQueueActions = + | TCommentQueueAction_ID | TCommentQueueAction_RemoveCommentID | TCommentQueueAction_RemoveCollectionRef | TCommentQueueAction_Update; +type TCommentQueueAction_ID = { + id: number; +}; type TCommentQueueAction_RemoveCommentID = { commentID: TCommentID; }; @@ -195,126 +219,113 @@ export type CPromptFormatOption_RadioData = string; // selected item string, e.g export type CPromptFormatOption_LikertData = string; // selected item string, e.g. '๐Ÿ’š' export type CPromptFormatOption_DiscreteSliderData = string; // selected item 0-based index e.g. "2" +// Generic Comment Type +// If DEFAULT_CommentTypes is not defined, fall back to using a basic comment. +const DEFAULT_COMMENTTYPE: TCommentType = { + slug: 'cmt', + label: 'Comment', // comment type label + prompts: [ + { + format: 'text', + prompt: 'Comment', // prompt label + help: 'Use this for any general comment.', // displayed below prompt + feedback: '' // displayed below input field + } + ] +}; +// DEFAULT_CommentTypes +// Use this to fall back to a common set of comment types for project templates without +// comment type definitions, e.g. if you're spinining up multiple servers from a common code base. +// This "burns in" basic DEFAULT_CommentTypes. +// Comment types defined in the the project template will override DEFAULT_CommentTypes. const DEFAULT_CommentTypes: Array = [ - // Add default comment type if none are defined + DEFAULT_COMMENTTYPE, { - slug: 'cmt', - label: 'Comment', // comment type label + slug: 'tellmemore', + label: 'Tell me more', // comment type label prompts: [ { format: 'text', - prompt: 'Comment', // prompt label - help: 'Use this for any general comment.', - feedback: '' + prompt: 'Please tell me more', // prompt label + help: 'Can you tell me more about ... ', // displayed below prompt + feedback: '' // displayed below input field } ] } - // Temporarily moved into template 2024-07-30 - // Move eventually to new templating system + // OTHER EXAMPLES of different types // // { - // slug: 'demo', - // label: 'Demo', + // slug: 'evidence', + // label: 'Evidence Critique or Suggestion', // prompts: [ // { - // format: 'text', - // prompt: 'Comment', // prompt label - // help: 'Use this for any general comment.', - // feedback: 'Just enter text' - // }, - // { // format: 'dropdown', - // prompt: 'How often did you use "Dropdown"', // prompt label - // options: ['๐Ÿฅฒ No', '๐Ÿค” A little', '๐Ÿ˜€ A lot'], - // help: 'Select one.', - // feedback: 'Single selection via dropdown menu' - // }, - // { - // format: 'checkbox', - // prompt: 'What types of fruit did you "Checkbox"?', // prompt label - // options: ['Apple Pie', 'Orange, Lime', 'Banana'], - // help: 'Select as many as you want.', - // feedback: 'Supports multiple selections' - // }, - // { - // format: 'radio', - // prompt: 'What do you think "Radio"?', // prompt label - // options: [ - // 'It makes sense', - // 'I disagree', - // "I don't know", - // 'Handle, comma, please' - // ], - // help: 'Select only one.', - // feedback: 'Mutually exclusive single selections' - // }, - // { - // format: 'likert', - // prompt: 'How did you like it "likert"?', // prompt label - // options: ['๐Ÿ’™', '๐Ÿ’š', '๐Ÿ’›', '๐Ÿงก', '๐Ÿฉท'], - // help: 'Select one of a series listed horizontally', - // feedback: 'Select with a single click. Supports emojis.' - // }, - // { - // format: 'discrete-slider', - // prompt: 'Star Rating "discrete-slider"?', // prompt label - // options: ['โ˜…', 'โ˜…', 'โ˜…', 'โ˜…', 'โ˜…'], - // help: 'Select one of a series stacked horizontally', - // feedback: 'Select with a single click. Supports emojis.' - // }, - // { - // format: 'text', - // prompt: 'Comment 2', // prompt label - // help: 'Use this for any general comment.', - // feedback: 'Just enter text' + // prompt: 'Is this supported by evidence?', // prompt label + // options: ['๐Ÿ˜€ Yes', '๐Ÿค” Some', '๐Ÿฅฒ No'], + // help: 'Select one.' // }, // { // format: 'text', - // prompt: 'Comment 3', // prompt label - // help: 'Use this for any general comment.', - // feedback: 'Just enter text' + // prompt: 'What would you change?', // prompt label + // help: 'Please be specific to help your friend.' // } // ] - // }, + // } // { - // slug: 'cmt', - // label: 'Comment', // comment type label + // slug: 'clarity', + // label: 'Clarity Critique or Suggestion', // prompts: [ // { + // format: 'discrete-slider', + // prompt: 'How clear is this model to you?', // prompt label + // options: ['โ˜…', 'โ˜…', 'โ˜…', 'โ˜…', 'โ˜…'], + // help: 'More stars means more clear!', + // feedback: 'We can also have help here' + // }, + // { // format: 'text', - // prompt: 'Comment', // prompt label - // help: 'Use this for any general comment.', - // feedback: '' + // prompt: 'What made you pick that number?', // prompt label + // help: 'Please be specific to help your friend.' // } // ] // }, // { - // slug: 'tellmemore', - // label: 'Tell me more', // comment type label + // slug: 'steps', + // label: 'All the Steps Critique or Suggestion', // prompts: [ // { + // format: 'dropdown', + // prompt: 'Does this include all of the useful steps?', // prompt label + // options: ['๐Ÿ˜€ Yes', '๐Ÿค” Mostly', '๐Ÿฅฒ No', '๐Ÿฅฒ Too many'], + // help: 'Select one.' + // }, + // { // format: 'text', - // prompt: 'Please tell me more', // prompt label - // help: 'Can you tell me more about ... ', - // feedback: '' + // prompt: 'What made you pick that number?', // prompt label + // help: 'Please be specific to help your friend.' // } // ] // }, // { - // slug: 'source', - // label: 'Source', // comment type label + // slug: 'response', + // label: 'Response', // prompts: [ // { - // format: 'text', - // prompt: 'Is this well sourced?', // prompt label - // help: 'Yes/No', - // feedback: '' + // format: 'radio', + // prompt: 'Do you agree with this comment, critique, or suggestion?', // prompt label + // options: ['Yes', 'Somewhat', 'No'], + // help: 'Select only one.' + // }, + // { + // format: 'radio', + // prompt: 'Will you make any changes?', // prompt label + // options: ['Yes', 'Some', 'No'], + // help: 'Select only one.' // }, // { // format: 'text', - // prompt: 'Changes', // prompt label - // help: 'What about the sourcing could be improved?', - // feedback: '' + // prompt: 'Why or why not?', // prompt label + // help: 'Please be specific so your friend understands.' // } // ] // } @@ -326,6 +337,7 @@ function m_LoadUsers(dbUsers: TUserObject[]) { dbUsers.forEach(u => USERS.set(u.id, u.name)); } function m_LoadCommentTypes(commentTypes: TCommentType[]) { + COMMENTTYPES.clear(); commentTypes.forEach(t => COMMENTTYPES.set(t.slug, t)); } function m_LoadComments(comments: TComment[]) { @@ -347,7 +359,14 @@ function Init() { } function LoadTemplate(commentTypes: Array) { - const types = commentTypes || DEFAULT_CommentTypes; // fall back to simple comment if it's not defined + // fall back order: template.toml > DEFAULT_CommentTypes > DEFAULT_COMMENTTYPE + // fall back to DEFAULT_CommentTypes if DEFAULT_CommentTypes is not in dc-comments + // or a single DEFAULT_COMMENTTYPE if it's completely missing + const types = + commentTypes || + (DEFAULT_CommentTypes && DEFAULT_CommentTypes.length > 0 + ? DEFAULT_CommentTypes + : [DEFAULT_COMMENTTYPE]); m_LoadCommentTypes(types); } @@ -360,9 +379,21 @@ function LoadTemplate(commentTypes: Array) { */ function LoadDB(data: TLokiData) { if (DBG) console.log(PR, 'LoadDB'); + USERS.clear(); + // COMMENTTYPES.clear(); // MEME reads types from db, but NC reads from template, so don't clear for NC + COMMENTS.clear(); + READBY.clear(); + ROOTS.clear(); + REPLY_ROOTS.clear(); + NEXT.clear(); + // Load Data! - if (data.commenttypes) m_LoadCommentTypes(data.commenttypes); - else m_LoadCommentTypes(DEFAULT_CommentTypes); // load default comments if db has none + + // MEME loads comment types in the db + // But NC relies on template to define comment types, so don't load commenttypes here for NC + // if (data.commenttypes) m_LoadCommentTypes(data.commenttypes); + // else m_LoadCommentTypes(DEFAULT_CommentTypes); // load default comments if db has none + if (data.users) m_LoadUsers(data.users); if (data.comments) m_LoadComments(data.comments); if (data.readby) m_LoadReadBy(data.readby); @@ -392,20 +423,27 @@ function GetCurrentUser(): TUserName { function GetCommentTypes(): TCommentTypeMap { return COMMENTTYPES; } -function GetCommentType(typeid): TCommentType { - return COMMENTTYPES.get(typeid); +function GetCommentType(slug: CType): TCommentType { + return COMMENTTYPES.get(slug); } function GetDefaultCommentType(): TCommentType { // returns the first comment type object - if (DEFAULT_CommentTypes.length < 1) + if (COMMENTTYPES.size < 1) throw new Error('dc-comments: No comment types defined!'); - return GetCommentType(DEFAULT_CommentTypes[0].slug); + return GetCommentType(GetDefaultCommentTypeSlug()); +} +function GetDefaultCommentTypeSlug(): CType { + // returns the first comment type slug + // fall back order: template.toml > DEFAULT_CommentTypes > DEFAULT_COMMENTTYPE + if (COMMENTTYPES.size < 1) + throw new Error('dc-comments: No comment types defined!'); + return COMMENTTYPES.keys().next().value; } function GetCOMMENTS(): TCommentMap { return COMMENTS; } -function GetComment(cid): TComment { +function GetComment(cid: TCommentID): TComment { return COMMENTS.get(cid); } @@ -440,7 +478,7 @@ function AddComment(data): TComment { comment_id: data.comment_id, // thread comment_id_parent, comment_id_previous, - comment_type: 'cmt', // default type, no prompts + comment_type: GetDefaultCommentTypeSlug(), // default type, no prompts comment_createtime: new Date().getTime(), comment_modifytime: null, comment_isMarkedDeleted: data.comment_isMarkedDeleted, @@ -470,10 +508,11 @@ function UpdateComment(cobj: TComment) { function m_UpdateComment(cobj: TComment) { // Fake modify date until we get DB roundtrip cobj.comment_modifytime = new Date().getTime(); - console.log( - 'REVIEW: UpdateComment...modify time should use loki time???', - cobj.comment_modifytime - ); + // console.log( + // 'REVIEW: UpdateComment...modify time should use loki time???', + // cobj.comment_id, + // cobj.comment_modifytime + // ); COMMENTS.set(cobj.comment_id, cobj); } /** @@ -485,6 +524,21 @@ function HandleUpdatedComments(cobjs: TComment[]) { m_DeriveValues(); } +/** + * Safely delete a comment and queue it for deletion + * This is necessary to also return the `id` + * @param {string} cid comment_id + * @returns {Object} TCommentQueueActions + */ +function m_safeDeleteAndQueue(cid): TCommentQueueActions { + if (COMMENTS.has(cid)) { + const cmt = COMMENTS.get(cid); + const id = cmt ? cmt.id : undefined; // this should not happen + COMMENTS.delete(cid); + return { id, commentID: cid }; + } + throw new Error(`Comment ${cid} not found. This should not happen!`); +} /** * @param {Object} parms * @param {Object} parms.collection_ref @@ -496,7 +550,7 @@ function HandleUpdatedComments(cobjs: TComment[]) { */ function RemoveComment(parms): TCommentQueueActions[] { const { collection_ref, comment_id, uid, isAdmin } = parms; - const queuedActions = []; + const queuedActions: TCommentQueueActions[] = []; // MAIN PROCESS: `xxxToDelete` // A. Determine the comment to remove @@ -564,8 +618,7 @@ function RemoveComment(parms): TCommentQueueActions[] { childThreadIds.push(cobj.comment_id); }); childThreadIds.forEach(cid => { - COMMENTS.delete(cid); - queuedActions.push({ commentID: cid }); + queuedActions.push(m_safeDeleteAndQueue(cid)); }); } @@ -574,8 +627,7 @@ function RemoveComment(parms): TCommentQueueActions[] { if (DBG) console.log(`deleteTargetAndNext`); const nextIds = m_GetNexts(cidToDelete); nextIds.forEach(cid => { - COMMENTS.delete(cid); - queuedActions.push({ commentID: cid }); + queuedActions.push(m_safeDeleteAndQueue(cid)); }); } @@ -601,8 +653,7 @@ function RemoveComment(parms): TCommentQueueActions[] { if (deleteTarget || deleteTargetAndNext || deleteRootAndChildren) { // DELETE TARGET if (DBG) console.log('deleteTarget or Root', cidToDelete); - COMMENTS.delete(cidToDelete); - queuedActions.push({ commentID: cidToDelete }); + queuedActions.push(m_safeDeleteAndQueue(cidToDelete)); } else if (markDeleted) { // MARK TARGET DELETED if (DBG) console.log('markDeleted', cidToDelete); @@ -643,15 +694,13 @@ function RemoveComment(parms): TCommentQueueActions[] { const replyIds = m_GetReplies(rootId); replyIds.forEach(cid => { if (COMMENTS.has(cid)) { - COMMENTS.delete(cid); - queuedActions.push({ commentID: cid }); + queuedActions.push(m_safeDeleteAndQueue(cid)); } }); // also delete the root if (COMMENTS.has(rootId)) { - COMMENTS.delete(rootId); - queuedActions.push({ commentID: rootId }); + queuedActions.push(m_safeDeleteAndQueue(rootId)); } } @@ -666,8 +715,7 @@ function RemoveComment(parms): TCommentQueueActions[] { const cobj = COMMENTS.get(cid); if (cobj && cobj.comment_isMarkedDeleted) { // is already marked deleted so remove it - COMMENTS.delete(cid); - queuedActions.push({ commentID: cid }); + queuedActions.push(m_safeDeleteAndQueue(cid)); } else if (cobj && !cobj.comment_isMarkedDeleted) { // found an undeleted item, stop! break; @@ -892,7 +940,7 @@ export default { IsMarkedDeleted, GetThreadedCommentIds, GetThreadedCommentData, - // GetThreadedCommentDataForRoot, + // GetThreadedCommentDataForRoot, // NOT USED? // READBY GetReadby, // ROOTS diff --git a/app-templates/_default.template.toml b/app-templates/_default.template.toml index c88398622..7a81395b1 100644 --- a/app-templates/_default.template.toml +++ b/app-templates/_default.template.toml @@ -28,6 +28,26 @@ sourceColor = "#FFa500" text = "No citation set" hidden = false +[[commentTypes]] +slug = "cmt" +label = "Comment" + + [[commentTypes.prompts]] + format = "text" + prompt = "Comment" + help = "Use this for any general comment." + feedback = "" + +[[commentTypes]] +slug = "tellmemore" +label = "Evidence Critique or Suggestion" + + [[commentTypes.prompts]] + format = "text" + prompt = "Tell me more" + help = "Can you tell me more about ... " + feedback = "" + [nodeDefs.id] type = "number" displayLabel = "Id" @@ -39,7 +59,7 @@ hidden = false [nodeDefs.label] type = "string" -displayLabel = "label" +displayLabel = "Label" exportLabel = "Label" help = "Display name of the node" includeInGraphTooltip = true @@ -78,7 +98,7 @@ isProvenance = false hidden = false [nodeDefs.infoSource] -type = "string" +type = "infoOrigin" displayLabel = "Info Source" exportLabel = "InfoSource" help = "Who created this? (aka Source)" @@ -100,7 +120,6 @@ displayLabel = "Created" exportLabel = "Created" help = "Date and time node was created" includeInGraphTooltip = true -isProvenance = true hidden = false [nodeDefs.createdBy] @@ -108,7 +127,6 @@ displayLabel = "Created By" help = "Author who created the node" exportLabel = "Created By" includeInGraphTooltip = true -isProvenance = true hidden = false [nodeDefs.updated] @@ -116,7 +134,6 @@ displayLabel = "Updated" exportLabel = "Updated" help = "Date and time node was last modified" includeInGraphTooltip = true -isProvenance = true hidden = false [nodeDefs.updatedBy] @@ -124,7 +141,6 @@ displayLabel = "Updated By" exportLabel = "Updated By" help = "Author who updated the node" includeInGraphTooltip = true -isProvenance = true hidden = false [nodeDefs.revision] @@ -132,7 +148,6 @@ displayLabel = "Revision" exportLabel = "Revision" help = "Number of times this node has been revised" includeInGraphTooltip = true -isProvenance = true hidden = true [edgeDefs.id] @@ -178,14 +193,6 @@ help = "Significance of the connection" isProvenance = false hidden = false -[edgeDefs.info] -type = "number" -displayLabel = "Date" -exportLabel = "Date" -help = '"YYYY-MM-DD" format"' -isProvenance = false -hidden = false - [edgeDefs.weight] type = "number" defaultValue = 1 @@ -227,7 +234,6 @@ displayLabel = "Created" exportLabel = "Created" includeInGraphTooltip = true help = "Date and time edge was created" -isProvenance = true hidden = false [edgeDefs.createdBy] @@ -235,7 +241,6 @@ displayLabel = "Created By" exportLabel = "Created By" includeInGraphTooltip = true help = "Author who created the edge" -isProvenance = true hidden = false [edgeDefs.updated] @@ -243,7 +248,6 @@ displayLabel = "Updated" exportLabel = "Updated" includeInGraphTooltip = true help = "Date and time edge was last modified" -isProvenance = true hidden = false [edgeDefs.updatedBy] @@ -251,7 +255,6 @@ displayLabel = "Updated By" exportLabel = "Updated By" includeInGraphTooltip = true help = "Author who updated the nodedgee" -isProvenance = true hidden = false [edgeDefs.revision] @@ -259,5 +262,4 @@ displayLabel = "Revision" exportLabel = "Revision" includeInGraphTooltip = true help = "Number of times this edge has been revised" -isProvenance = true hidden = true diff --git a/app/system/util/hdate.js b/app/system/util/hdate.js index 86168ef7d..20125bb66 100644 --- a/app/system/util/hdate.js +++ b/app/system/util/hdate.js @@ -3,7 +3,7 @@ Historical Date Utilities Used by the URDateField component to parse and format historical dates. - Also used in NodeTablea nd EdgeTable for sorting and filtering. + Also used in NodeTablea and EdgeTable for sorting and filtering. \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ import * as chrono from 'chrono-node'; @@ -17,19 +17,19 @@ const HDATE = {}; HDATE.erasChrono = chrono.casual.clone(); HDATE.erasChrono.parsers.push( { - pattern: () => { + pattern: () => { // match year if "BC/AD/BCE/CE" (case insensitive) // "10bc" -- without space // "10 bc" -- with space return /(\d+)\s*(BCE|CE|BC|AD)?/i; - }, - extract: (context, match) => { - const year = parseInt(match[1], 10); + }, + extract: (context, match) => { + const year = parseInt(match[1], 10); const era = match[2] ? match[2].toUpperCase() : 'CE'; // default to CE - // Adjust the year based on the era - const adjustedYear = era === 'BCE' || era === 'BC' ? -year : year; - return { year: adjustedYear }; - } + // Adjust the year based on the era + const adjustedYear = era === 'BCE' || era === 'BC' ? -year : year; + return { year: adjustedYear }; + } } // ALTERNATIVE APPROACH: Any 3 digits is a year @@ -52,14 +52,12 @@ HDATE.erasChrono.parsers.push( // } // }, ); -HDATE.erasChrono.refiners.push( - { - refine: (context, results) => { - // placeholder if we decide we want to add a refiner - return results; - } +HDATE.erasChrono.refiners.push({ + refine: (context, results) => { + // placeholder if we decide we want to add a refiner + return results; } -); +}); /// CONSTANTS & DECLARATIONS ////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -141,28 +139,28 @@ HDATE.DATEFORMAT = { HDATE.u_pad = num => { if (!num) return ''; return num.toString().padStart(2, '0'); -} +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - HDATE.u_monthAbbr = num => { const date = new Date(2000, num - 1, 1); return date.toLocaleDateString('default', { month: 'short' }); -} +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - HDATE.u_monthName = num => { const date = new Date(2000, num - 1, 1); return date.toLocaleDateString('default', { month: 'long' }); -} +}; /// HDATE METHODS ////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - HDATE.Parse = dateInputStr => { return HDATE.erasChrono.parse(dateInputStr); -} +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Show how the raw input string is parsed into date information by breaking - * down the known values (e.g. `day`, and `month`) into a human-readable string. - * @param {Array} ParsedResult - a chrono array of parsed date objects - */ + * down the known values (e.g. `day`, and `month`) into a human-readable string. + * @param {Array} ParsedResult - a chrono array of parsed date objects + */ HDATE.ShowValidationResults = ParsedResult => { // Show interpreted values if (ParsedResult.length > 0) { @@ -174,7 +172,7 @@ HDATE.ShowValidationResults = ParsedResult => { return dateValidationStr; } return undefined; -} +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Show the list of available format types with previews based on the values * parsed from the input string. e.g. `April 1, 2024` will show formats @@ -186,9 +184,7 @@ HDATE.ShowMatchingFormats = (ParsedResult, dateFormat, allowFormatSelection) => let options = [{ value: 'AS_ENTERED', preview: 'as entered' }]; if (ParsedResult.length < 1) { if (allowFormatSelection) return options; - else return [ - { value: dateFormat, preview: HDATE.DATEFORMAT[dateFormat] } - ]; + else return [{ value: dateFormat, preview: HDATE.DATEFORMAT[dateFormat] }]; } let matchingTypes = []; @@ -200,7 +196,10 @@ HDATE.ShowMatchingFormats = (ParsedResult, dateFormat, allowFormatSelection) => if (!allowFormatSelection) { // force the format to use the defined format const options = [ - { value: dateFormat, preview: HDATE.GetPreviewStr(dateInputStr, knownValues, dateFormat) } + { + value: dateFormat, + preview: HDATE.GetPreviewStr(dateInputStr, knownValues, dateFormat) + } ]; return options; } @@ -267,11 +266,14 @@ HDATE.ShowMatchingFormats = (ParsedResult, dateFormat, allowFormatSelection) => } additionalOptions = matchingTypes.map(type => { - return { value: type, preview: HDATE.GetPreviewStr(dateInputStr, knownValues, type) }; + return { + value: type, + preview: HDATE.GetPreviewStr(dateInputStr, knownValues, type) + }; }); options = [...additionalOptions, ...options]; return options; -} +}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Show the formatted date string using the parsed result information * @param {String} dateInputStr - the raw input string @@ -330,7 +332,9 @@ HDATE.GetPreviewStr = (dateInputStr, knownValues, format) => { : `${month}/${day}/${year} ${HDATE.ERAS.post}`; case 'HISTORICAL_MONTHDAYYEAR_PAD': return year < 1 - ? `${HDATE.u_pad(month)}/${HDATE.u_pad(day)}/${Math.abs(year)} ${HDATE.ERAS.pre}` + ? `${HDATE.u_pad(month)}/${HDATE.u_pad(day)}/${Math.abs(year)} ${ + HDATE.ERAS.pre + }` : `${HDATE.u_pad(month)}/${HDATE.u_pad(day)}/${year} ${HDATE.ERAS.post}`; case 'HISTORICAL_YEARMONTHDAY_ABBR': return year < 1 @@ -346,7 +350,9 @@ HDATE.GetPreviewStr = (dateInputStr, knownValues, format) => { : `${year}/${month}/${day} ${HDATE.ERAS.post}`; case 'HISTORICAL_YEARMONTHDAY_PAD': return year < 1 - ? `${Math.abs(year)}/${HDATE.u_pad(month)}/${HDATE.u_pad(day)} ${HDATE.ERAS.pre}` + ? `${Math.abs(year)}/${HDATE.u_pad(month)}/${HDATE.u_pad(day)} ${ + HDATE.ERAS.pre + }` : `${year}/${HDATE.u_pad(month)}/${HDATE.u_pad(day)} ${HDATE.ERAS.post}`; case 'MONTHYEAR_ABBR': return `${HDATE.u_monthAbbr(month)} ${year}`; @@ -399,13 +405,15 @@ HDATE.GetPreviewStr = (dateInputStr, knownValues, format) => { case 'YEAR': return `${year}`; case 'HISTORICALYEAR': - return year < 1 ? `${Math.abs(year)} ${HDATE.ERAS.pre}` : `${year} ${HDATE.ERAS.post}`; + return year < 1 + ? `${Math.abs(year)} ${HDATE.ERAS.pre}` + : `${year} ${HDATE.ERAS.post}`; case 'AS_ENTERED': default: // console.log('showprevieow...showing as entered', dateInputStr); return `${dateInputStr}` || '...'; } -} +}; /// MODULE EXPORTS //////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/view/netcreate/NetCreate.css b/app/view/netcreate/NetCreate.css index 4b04e78da..b160c084c 100644 --- a/app/view/netcreate/NetCreate.css +++ b/app/view/netcreate/NetCreate.css @@ -9,6 +9,7 @@ 2000 comment-bar 1500 FiltersPanel 1030 bootstrap .fixed-top + 101 URTable tabletip 100 Node/EdgeTable thead 15 NCAutoSuggest .matchlist 15 NCNode .ncedge @@ -118,4 +119,5 @@ button svg { left: 1px; right: 10px; background-color: #eafcff; + font-size: 0.75em; } diff --git a/app/view/netcreate/comment-mgr.js b/app/view/netcreate/comment-mgr.js index d1b62e026..7d3ea32b8 100644 --- a/app/view/netcreate/comment-mgr.js +++ b/app/view/netcreate/comment-mgr.js @@ -23,6 +23,8 @@ const SETTINGS = require('settings'); const DBG = true; const PR = 'comment-mgr: '; +const CMTBTNOFFSET = 10; + /// INITIALIZE MODULE ///////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - let MOD = UNISYS.NewModule(module.id); @@ -78,103 +80,45 @@ MOD.Hook('APP_READY', function (info) { /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MOD.LoadDB = data => { const TEMPLATE = UDATA.AppState('TEMPLATE'); - COMMENT.LoadTemplate(TEMPLATE.COMMENTTYPES); + COMMENT.LoadTemplate(TEMPLATE.commentTypes); COMMENT.LoadDB(data); + + // After loading db, derive the view objects + // This is needed to force update of the project comment count + const uid = MOD.GetCurrentUserId(); + COMMENT.DeriveAllThreadedViewObjects(uid); + const COMMENTCOLLECTION = COMMENT.GetCommentCollections(); + UDATA.SetAppState('COMMENTCOLLECTION', COMMENTCOLLECTION); }; /// HELPER FUNCTIONS ////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -MOD.COMMENTICON = ( - - - -); // From Evan O'Neil https://drive.google.com/drive/folders/1fJ5WiLMVQxxaqghrCOFwegmnYoOvst7E -// NOTE viewbox is set to y=1 to better center the text -MOD.ICN_COMMENT_UNREAD = ( - - - - -); -MOD.ICN_COMMENT_UNREAD_SELECTED = ( - - - - -); -MOD.ICN_COMMENT_READ = ( - - - - -); -MOD.ICN_COMMENT_READ_SELECTED = ( - +MOD.COMMENTICON = ( + + > - + className="svg-outline" + d="M3.17778 10.8696V13.6H8C11.0928 13.6 13.6 11.0928 13.6 8C13.6 4.90721 11.0928 2.4 8 2.4C4.90721 2.4 2.4 4.90721 2.4 8C2.4 8.92813 2.62469 9.79968 3.02143 10.5671L3.17778 10.8696ZM15 8C15 11.866 11.866 15 8 15H1.77778V11.21C1.28072 10.2485 1 9.15705 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8Z" + > + + ); - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_SetAppStateCommentCollections() { const COMMENTCOLLECTION = COMMENT.GetCommentCollections(); UDATA.SetAppState('COMMENTCOLLECTION', COMMENTCOLLECTION); } - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_SetAppStateCommentVObjs() { const COMMENTVOBJS = COMMENT.GetCOMMENTVOBJS(); UDATA.SetAppState('COMMENTVOBJS', COMMENTVOBJS); } - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_UpdateComment(comment) { const cobj = { collection_ref: comment.collection_ref, @@ -191,17 +135,17 @@ function m_UpdateComment(comment) { const uid = MOD.GetCurrentUserId(); COMMENT.UpdateComment(cobj, uid); } - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_UpdatePermissions(data) { UDATA.NetCall('SRV_GET_EDIT_STATUS').then(data => { // disable comment button if someone is editing a comment UDATA.LocalCall('COMMENT_UPDATE_PERMISSIONS', data); }); } + /// API METHODS /////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// CONSTANTS MOD.VIEWMODE = NCUI.VIEWMODE; @@ -213,12 +157,13 @@ MOD.GetEdgeCREF = edgeId => `e${edgeId}`; MOD.GetProjectCREF = projectId => `p${projectId}`; /// deconstructs "n32" into {type: "n", id: 32} -MOD.DeconstructCref = cref => { +MOD.DeconstructCREF = cref => { const type = cref.substring(0, 1); const id = cref.substring(1); return { type, id }; }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Generate a human friendly label based on the cref (e.g. `n21`, `e4`) * e.g. "n32" becomes {typeLabel "Node", sourceLabel: "32"} @@ -226,7 +171,7 @@ MOD.DeconstructCref = cref => { * @returns { typeLabel, sourceLabel } sourceLabel is undefined if the source has been deleted */ MOD.GetCREFSourceLabel = cref => { - const { type, id } = MOD.DeconstructCref(cref); + const { type, id } = MOD.DeconstructCREF(cref); let typeLabel; let node, edge, nodes, sourceNode, targetNode; let sourceLabel; // undefined if not found @@ -248,17 +193,54 @@ MOD.GetCREFSourceLabel = cref => { sourceLabel = `${sourceNode.label}${ARROW_RIGHT}${targetNode.label}`; break; case 'p': - typeLabel = 'Project'; + typeLabel = 'Project'; // reserve for future use sourceLabel = id; break; } return { typeLabel, sourceLabel }; }; - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** + * Returns the position for the comment button + * Adjusting for window position is done via GetCommentCollectionPosition + */ +MOD.GetCommentBtnPosition = cref => { + const btn = document.getElementById(cref); + if (!btn) + throw new Error(`${PR}GetCommentCollectionPosition: Button not found ${cref}`); + const bbox = btn.getBoundingClientRect(); + return { x: bbox.left, y: bbox.top }; +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** + * Returns the comment window position for the comment button + * shifting the window to the left if it's too close to the edge of the screen. + * or shifting it up if it's too close to the bottom of the screen. + * x,y is the position of the comment button, offsets are then caclulated + */ +MOD.GetCommentCollectionPosition = ({ x, y }, isExpanded) => { + const windowWidth = Math.min(screen.width, window.innerWidth); + const windowHeight = Math.min(screen.height, window.innerHeight); + let newX; + if (windowWidth - x < 500) { + newX = x - 410; + } else { + newX = x + CMTBTNOFFSET * 2; + } + let newY = y + window.scrollY; + if (windowHeight - y < 250) { + if (isExpanded) newY = y - 250; + else newY = y - 150; + } else { + newY = y - CMTBTNOFFSET; + } + return { x: newX, y: newY }; +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Open the object that the comment refers to /// e.g. in Net.Create it's a node or edge object MOD.OpenReferent = cref => { - const { type, id } = MOD.DeconstructCref(cref); + const { type, id } = MOD.DeconstructCREF(cref); let edge; switch (type) { case 'n': @@ -270,15 +252,15 @@ MOD.OpenReferent = cref => { UDATA.LocalCall('EDGE_SELECT', { edgeId: edge.id }); }); break; - case 'p': + case 'p': // reserve for future use // do something? break; } }; - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Open comment using a comment id MOD.OpenComment = (cref, cid) => { - const { type, id } = MOD.DeconstructCref(cref); + const { type, id } = MOD.DeconstructCREF(cref); let edge; switch (type) { case 'n': @@ -300,7 +282,7 @@ MOD.OpenComment = (cref, cid) => { }); }); break; - case 'p': + case 'p': // reserve for future use // do something? break; } @@ -342,44 +324,178 @@ MOD.MarkAllRead = () => { m_SetAppStateCommentCollections(); }; +/// COMMENT COLLECTIONS /////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Comment Collections MOD.GetCommentCollection = uiref => { return COMMENT.GetCommentCollection(uiref); }; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -MOD.OpenCommentCollection = (uiref, cref, position) => { - MOD.UpdateCommentUIState(uiref, { cref, isOpen: true }); - UDATA.LocalCall('CTHREADMGR_THREAD_OPENED', { uiref, cref, position }); +/* + OpenCommentCollection + + The requests come from four sources: + * Node Editor + * Edge Editor + * Node Table + * Edge Table + + URCommentVBtn is a UI component that passes clicks + to URCommentCollectionMgr via UR.Publish(`CMT_COLLECTION_SHOW`) calls + + URCommentSVGBtn is a purely visual component that renders SVG buttons + as symbols and displays the comment count and selection status. + It pases the click events to URCommentVBtn. + + MAP + * URCommentStatus + > URCommentCollectionMgr + > URCommentThread + > URCommentVBtn + > URCommentSVGBtn + + + HOW IT WORKS + When a Node Editor, Edge Editor, Node Table, or Edge Table clicks on the + URCommentVBtn, URCommentCollectionMgr will: + * Add the requested Thread to the URCommentCollectionMgr + * Open the URCommentThread + * When the URCommentThread is closed, it will be removed from the URCommentCollectionMgr + +*/ +MOD.OpenCommentCollection = (cref, position) => { + // Validate + if (cref === undefined) + throw new Error( + `comment-mgr.OpenCommentCollection: missing cref data ${JSON.stringify(cref)}` + ); + if (position === undefined || position.x === undefined || position.y === undefined) + throw new Error( + `comment-mgr.OpenCommentCollection: missing position data ${JSON.stringify( + position + )}` + ); + position.x = parseInt(position.x); // handle net call data + position.y = parseInt(position.y); + // 0. If the comment is already open, do nothing + const openComments = MOD.GetOpenComments(cref); + if (openComments) { + MOD.CloseCommentCollection(cref, cref, MOD.GetCurrentUserId()); + return; // already open, close it + } + // 1. Position the window to the right of the click + const commentThreadWindowIsExpanded = MOD.GetCommentCollectionCount(cref); + const collectionPosition = MOD.GetCommentCollectionPosition( + position, + commentThreadWindowIsExpanded + ); + + // 2. Update the state + MOD.UpdateCommentUIState(cref, { cref, isOpen: true }); + // 3. Open the collection in the collection manager + UDATA.LocalCall('CMT_COLLECTION_SHOW', { cref, position: collectionPosition }); }; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - * Used by NCNodeTable and NCEdgeTable to open/close the comment thread - * If a comment is already opened by one button (e.g. node), and the user - * clicks on another comment button (e.g. NodeTable), the new one will open, - * and the old one closed. - * Called by URCommentBtn, NCNodeTable, and NCEdgeTable - * @param {TCommentUIRef} uiref comment button id - * @param {TCollectionRef} cref collection_ref - * @param {Object} position x, y position of the comment button + * Called by URCommentVBtn + * @param {string} cref */ -MOD.ToggleCommentCollection = (uiref, cref, position) => { - const uid = MOD.GetCurrentUserId(); - // is the comment already open? - const open_uiref = MOD.GetOpenComments(cref); - if (open_uiref === uiref) { - // already opened by THIS uiref, so toggle it closed. - MOD.CloseCommentCollection(uiref, cref, uid); - } else if (open_uiref !== undefined) { - // already opened by SOMEONE ELSE, so close it, then open the new one - MOD.CloseCommentCollection(open_uiref, cref, uid); - MOD.OpenCommentCollection(uiref, cref, position); - } else { - // no comment is open, so open the new one - MOD.OpenCommentCollection(uiref, cref, position); +MOD.OpenCommentCollectionByCref = cref => { + const cmtPosition = MOD.GetCommentBtnPosition(cref); + MOD.OpenCommentCollection(cref, { + x: cmtPosition.x + CMTBTNOFFSET, + y: cmtPosition.y + CMTBTNOFFSET + }); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// Open comment inside a collection using a comment id +/// NOTE this is NOT used by SVGButtons +MOD.OpenCommentStatusComment = (cref, cid) => { + const { type, id } = MOD.DeconstructCREF(cref); + let parms; + + // if a comment is being edited... + // - don't close all comments + // - don't open a new one + if (MOD.GetCommentsAreBeingEdited()) { + UR.Publish('DIALOG_OPEN', { + text: `Please finish editing your comment before opening a different comment!` + }); + return; + } + + MOD.CloseAllCommentCollectionsWithoutMarkingRead(); + + let edge; + switch (type) { + case 'p': // project (from MEME, currently not used) reserved for future use + MOD.OpenCommentCollectionByCref('projectcmt'); + break; + case 'n': + UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [parseInt(id)] }).then(() => { + MOD.OpenCommentCollectionByCref(cref); + // wait for the comment to open before scrolling to the current comment + // REVIEW: Do this as a callback? + // Problem is that this is a long chain for the callback + // - OpenCommentCollectionByCref + // - OpenCommentCollection + // - UpdateCommentUIState + // - m_SetAppStateCommentCollections + // - UDATA.SetAppState('COMMENTCOLLECTION) + setTimeout(() => { + const commentEl = document.getElementById(cid); + commentEl.scrollIntoView({ behavior: 'smooth' }); + }, 100); + }); + break; + case 'e': + edge = UDATA.AppState('NCDATA').edges.find(e => e.id === Number(id)); + UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [edge.source] }).then(() => { + UDATA.LocalCall('EDGE_SELECT', { edgeId: edge.id }).then(() => { + MOD.OpenCommentCollectionByCref(cref); + // wait for the comment to open before scrolling to the current comment + // REVIEW: Do this as a callback? + setTimeout(() => { + const commentEl = document.getElementById(cid); + commentEl.scrollIntoView({ behavior: 'smooth' }); + }); + }); + }); + break; } }; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// DEPRECATED -- URCommentVBtn handles this currently +/// But we might want to restore the ability to toggle in place. +// /** +// * Used by NCNodeTable and NCEdgeTable to open/close the comment thread +// * If a comment is already opened by one button (e.g. node), and the user +// * clicks on another comment button (e.g. NodeTable), the new one will open, +// * and the old one closed. +// * Called by URCommentBtn, NCNodeTable, and NCEdgeTable +// * @param {TCommentUIRef} uiref comment button id +// * @param {TCollectionRef} cref collection_ref +// * @param {Object} position x, y position of the comment button +// */ +// MOD.ToggleCommentCollection = (uiref, cref, position) => { +// const uid = MOD.GetCurrentUserId(); +// // is the comment already open? +// const open_uiref = MOD.GetOpenComments(cref); +// if (open_uiref === uiref) { +// // already opened by THIS uiref, so toggle it closed. +// MOD.CloseCommentCollection(uiref, cref, uid); +// } else if (open_uiref !== undefined) { +// // already opened by SOMEONE ELSE, so close it, then open the new one +// MOD.CloseCommentCollection(open_uiref, cref, uid); +// // REVIEW remove uiref? +// MOD.OpenCommentCollection(uiref, cref, position); +// } else { +// // no comment is open, so open the new one +// // REVIEW remove uiref? +// MOD.OpenCommentCollection(uiref, cref, position); +// } +// }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Marks a comment as read, and closes the component. * Called by NCCommentBtn when clicking "Close" @@ -396,11 +512,49 @@ MOD.CloseCommentCollection = (uiref, cref, uid) => { return; } // OK to close + UDATA.LocalCall('CMT_COLLECTION_HIDE', { cref }); + COMMENT.CloseCommentCollection(uiref, cref, uid); + m_SetAppStateCommentCollections(); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** + * Marks a comment as read, and closes the component. + * Called by NCCommentBtn when clicking "Close" + * @param {Object} uiref comment button id (note kept for Net.Create compatibility) + * @param {Object} cref collection_ref + * @param {Object} uid user id + */ +MOD.CloseCommentCollectionAndMarkRead = (uiref, cref, uid) => { + if (!MOD.OKtoClose(cref)) { + // Comment is still being edited, prevent close + alert( + 'This comment is still being edited! Please Save or Cancel before closing the comment.' + ); + return; + } + // OK to close + UDATA.LocalCall('CMT_COLLECTION_HIDE', { cref }); + // Update the readby m_DBUpdateReadBy(cref, uid); COMMENT.CloseCommentCollection(uiref, cref, uid); m_SetAppStateCommentCollections(); - // call to broadcast state AFTER derived state changes - UDATA.LocalCall('CTHREADMGR_THREAD_CLOSED', { cref }); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** + * Closes all comment collections without marking them as read. + * Used by comment status when user clicks on status updates to display + * updated comments. + * @param {*} uid + */ +MOD.CloseAllCommentCollectionsWithoutMarkingRead = () => { + const uid = MOD.GetCurrentUserId(); + UDATA.LocalCall('CMT_COLLECTION_HIDE_ALL'); + COMMENT.CloseAllCommentCollections(uid); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +MOD.GetCommentCollectionCount = cref => { + const ccol = COMMENT.GetCommentCollection(cref); + return ccol ? ccol.commentCount : ''; }; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MOD.GetCommentStats = () => { @@ -431,6 +585,7 @@ MOD.GetCommentThreadPosition = commentButtonId => { MOD.GetCommentUIState = uiref => { return COMMENT.GetCommentUIState(uiref); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Used to open/close the comment thread * @param {string} uiref @@ -446,8 +601,26 @@ MOD.UpdateCommentUIState = (uiref, openState) => { MOD.GetOpenComments = cref => COMMENT.GetOpenComments(cref); /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/// Editable Comments (comments being ddited) +/// Editable Comments (comments being edited) +MOD.RegisterCommentBeingEdited = cid => { + COMMENT.RegisterCommentBeingEdited(cid); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +MOD.DeRegisterCommentBeingEdited = cid => { + return COMMENT.DeRegisterCommentBeingEdited(cid); +}; + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// Are ANY comments being edited? +/// Returns True if ANY comment is being edited +/// * Used by comment status when user clicks on a comment id to view a saved comment +/// to prevent closing the comment collection if a comment is being edited. +/// * Also used by URCommentThread to determine whether "Click to add" is displayed +MOD.GetCommentsAreBeingEdited = () => { + return COMMENT.GetCommentsAreBeingEdited(); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MOD.OKtoClose = cref => { const cvobjs = MOD.GetThreadedViewObjects(cref); let isBeingEdited = false; @@ -462,9 +635,6 @@ MOD.OKtoClose = cref => { MOD.GetThreadedViewObjects = (cref, uid) => { return COMMENT.GetThreadedViewObjects(cref, uid); }; -MOD.GetThreadedViewObjectsCount = (cref, uid) => { - return COMMENT.GetThreadedViewObjectsCount(cref, uid); -}; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Comment View Objects @@ -477,14 +647,16 @@ MOD.GetCommentVObj = (cref, cid) => { MOD.GetComment = cid => { return COMMENT.GetComment(cid); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MOD.GetUnreadRepliesToMe = uid => { return COMMENT.GetUnreadRepliesToMe(uid); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MOD.GetUnreadComments = () => { return COMMENT.GetUnreadComments(); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - * * @param {Object} cobj Comment Object */ MOD.AddComment = cobj => { @@ -495,6 +667,34 @@ MOD.AddComment = cobj => { m_SetAppStateCommentVObjs(); }); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** User clicks Edit on a comment + * @param {TCommentID} comment_id + */ +MOD.UIEditComment = comment_id => { + MOD.RegisterCommentBeingEdited(comment_id); + MOD.LockComment(comment_id); + UDATA.NetCall('COMMENT_UPDATE_PERMISSIONS'); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** User clicks Cancel on a comment + * @param {TCommentID} comment_id + */ +MOD.UICancelComment = comment_id => { + MOD.DeRegisterCommentBeingEdited(comment_id); + MOD.UnlockComment(comment_id); + UDATA.NetCall('COMMENT_UPDATE_PERMISSIONS'); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/** User clicks Save coment + * @param {TComment} cobj + */ +MOD.UISaveComment = cobj => { + MOD.UnlockComment(cobj.comment_id); + MOD.DeRegisterCommentBeingEdited(cobj.comment_id); + MOD.UpdateComment(cobj); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Update the ac/dc comments, then save it to the db * This will also broadcast COMMENT_UPDATE so other clients on the network @@ -506,6 +706,7 @@ MOD.UpdateComment = cobj => { m_DBUpdateComment(cobj); m_SetAppStateCommentVObjs(); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Removing a comment can affect multiple comments, so this is done * via a batch operation. We queue up all of the comment changes @@ -515,42 +716,57 @@ MOD.UpdateComment = cobj => { * * Removing is a two step process: * 1. Show confirmation dialog - * 2. Execute the remova + * 2. Execute the removal + * + * Also used by "Cancel" button to remove a comment being edited * @param {Object} parms * @param {string} parms.collection_ref * @param {string} parms.comment_id + * @param {string} parms.id * @param {string} parms.uid + * @param {boolean} parms.isAdmin * @param {boolean} parms.showCancelDialog - * @param {function} cb CallBack + * @param {boolean} parms.skipDialog */ -MOD.RemoveComment = (parms, cb) => { +MOD.RemoveComment = parms => { let confirmMessage, okmessage, cancelmessage; if (parms.showCancelDialog) { // Are you sure you want to cancel? - confirmMessage = `Are you sure you want to cancel editing this comment #${parms.comment_id}?`; + confirmMessage = `Are you sure you want to cancel editing this comment #${parms.id}?`; okmessage = 'Cancel Editing and Delete'; cancelmessage = 'Go Back to Editing'; } else { + // show delete confirmaiton dialog // Are you sure you want to delete? parms.isAdmin = SETTINGS.IsAdmin(); confirmMessage = parms.isAdmin - ? `Are you sure you want to delete this comment #${parms.comment_id} and ALL related replies (admin only)?` - : `Are you sure you want to delete this comment #${parms.comment_id}?`; + ? `Are you sure you want to delete this comment #${parms.id} and ALL related replies (admin only)?` + : `Are you sure you want to delete this comment #${parms.id}?`; okmessage = 'Delete'; cancelmessage = "Don't Delete"; } - const dialog = ( - m_ExecuteRemoveComment(event, parms, cb)} - cancelmessage={cancelmessage} - onCancel={m_CloseRemoveCommentDialog} - /> - ); - const container = document.getElementById(dialogContainerId); - ReactDOM.render(dialog, container); + + const CMTSTATUS = UDATA.AppState('CMTSTATUS'); + if (parms.skipDialog) { + m_ExecuteRemoveComment(event, parms); + } else { + CMTSTATUS.dialog = { + isOpen: true, + message: confirmMessage, + okmessage, + onOK: event => m_ExecuteRemoveComment(event, parms), + cancelmessage, + onCancel: m_CloseRemoveCommentDialog + }; + } + UDATA.SetAppState('CMTSTATUS', CMTSTATUS); + + MOD.DeRegisterCommentBeingEdited(parms.comment_id); + MOD.UnlockComment(parms.comment_id); + + UDATA.LocalCall('COMMENT_UPDATE_PERMISSIONS'); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * The db call is made AFTER ac/dc handles the removal and the logic of * relinking comments. The db call is dumb, all the logic is in dc-comments. @@ -567,11 +783,13 @@ function m_ExecuteRemoveComment(event, parms, cb) { m_CloseRemoveCommentDialog(); if (typeof cb === 'function') cb(); } +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_CloseRemoveCommentDialog() { - const container = document.getElementById(dialogContainerId); - ReactDOM.unmountComponentAtNode(container); + const CMTSTATUS = UDATA.AppState('CMTSTATUS'); + CMTSTATUS.dialog = { isOpen: false }; + UDATA.SetAppState('CMTSTATUS', CMTSTATUS); } - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Requested when a node/edge is deleted * @param {string} cref @@ -586,7 +804,6 @@ MOD.RemoveAllCommentsForCref = cref => { /// EVENT HANDLERS //////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Respond to network COMMENTS_UPDATE Messages * Usually used after a comment deletion to handle a batch of comment updates @@ -621,6 +838,7 @@ MOD.HandleCOMMENTS_UPDATE = dataArray => { m_SetAppStateCommentCollections(); m_SetAppStateCommentVObjs(); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Respond to COMMENT_UPDATE Messages from the network * After the server/db saves the new/updated comment, COMMENT_UPDATE is @@ -632,12 +850,49 @@ MOD.HandleCOMMENTS_UPDATE = dataArray => { */ MOD.HandleCOMMENT_UPDATE = data => { if (DBG) console.log('COMMENT_UPDATE======================', data); - const { comment } = data; - m_UpdateComment(comment); - // and broadcast a state change - m_SetAppStateCommentCollections(); - m_SetAppStateCommentVObjs(); + + // If a new comment is sent over the network + // and the incoming comment conflicts with a comment being edited + // then re-link the editing comment to point to the incoming comment + + const { comment: incomingComment } = data; + const editingCommentId = COMMENT.GetCommentsBeingEdited().values().next().value; + const editingComment = COMMENT.GetComment(editingCommentId); + + if (editingComment) { + // conflict if both think they're the root + if ( + incomingComment.comment_id_parent === '' && + incomingComment.comment_id_previous === '' && + editingComment.comment_id_parent === '' && + editingComment.comment_id_previous === '' + ) { + if (DBG) console.error('CONFLICT! both think they are root'); + // Re-link the comment to the incoming + editingComment.comment_id_previous = incomingComment.comment_id; + } + // conflict if previous of both are the same + if (incomingComment.comment_id_previous === editingComment.comment_id_previous) { + if (DBG) console.error('CONFLICT! both think they are reply to same previous'); + // Re-link the comment to the incoming + editingComment.comment_id_previous = incomingComment.comment_id; + } + // conflict if parent of both are the same and previous are blank (new reply root) + if ( + incomingComment.comment_id_parent === editingComment.comment_id_parent && + incomingComment.comment_id_previous === '' && + editingComment.comment_id_previous === '' + ) { + if (DBG) console.error('CONFLICT! both think they are reply to same parent'); + // Re-link the comment to the incoming + editingComment.comment_id_previous = incomingComment.comment_id; + } + } + + const updatedComments = [{ comment: incomingComment }, { comment: editingComment }]; + MOD.HandleCOMMENTS_UPDATE(updatedComments); }; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MOD.HandleREADBY_UPDATE = data => { if (DBG) console.log('READBY_UPDATE======================'); // Not used currently @@ -664,7 +919,7 @@ MOD.UnlockComment = comment_id => { UDATA.LocalCall('SELECTMGR_SET_MODE', { mode: 'normal' }); }); }; - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_DBUpdateComment(cobj, cb) { const comment = { collection_ref: cobj.collection_ref, @@ -682,6 +937,7 @@ function m_DBUpdateComment(cobj, cb) { if (typeof cb === 'function') cb(data); }); } +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_DBUpdateReadBy(cref, uid) { // Get existing readby const cvobjs = COMMENT.GetThreadedViewObjects(cref, uid); @@ -700,6 +956,7 @@ function m_DBUpdateReadBy(cref, uid) { if (typeof cb === 'function') cb(data); }); } +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** * Executes multiple database operations via a batch of commands: * - `cobjs` will be updated diff --git a/app/view/netcreate/components/InfoPanel.jsx b/app/view/netcreate/components/InfoPanel.jsx index 1994bcc84..c08bf9da3 100644 --- a/app/view/netcreate/components/InfoPanel.jsx +++ b/app/view/netcreate/components/InfoPanel.jsx @@ -19,8 +19,8 @@ const React = require('react'); const ReactStrap = require('reactstrap'); const { TabContent, TabPane, Nav, NavItem, NavLink, Row, Col, Button } = ReactStrap; const classnames = require('classnames'); -const NCNodeTable = require('./NCNodeTable'); -const NCEdgeTable = require('./NCEdgeTable'); +import NCNodeTable from './NCNodeTable'; +import NCEdgeTable from './NCEdgeTable'; const More = require('./More'); /// CONSTANTS & DECLARATIONS ////////////////////////////////////////////////// diff --git a/app/view/netcreate/components/NCEdge.jsx b/app/view/netcreate/components/NCEdge.jsx index 62116be2b..f771b0b89 100644 --- a/app/view/netcreate/components/NCEdge.jsx +++ b/app/view/netcreate/components/NCEdge.jsx @@ -38,7 +38,7 @@ const NCAutoSuggest = require('./NCAutoSuggest'); const NCDialog = require('./NCDialog'); const NCDialogCitation = require('./NCDialogCitation'); const SETTINGS = require('settings'); -import URCommentBtn from './URCommentBtn'; +import URCommentVBtn from './URCommentVBtn'; /// CONSTANTS & DECLARATIONS ////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -176,8 +176,8 @@ class NCEdge extends UNISYS.Component { sourceId: null, targetId: null, type: '', - attributes: [], - provenance: [], + attributes: {}, + provenance: {}, created: undefined, updated: undefined, revision: 0, @@ -262,14 +262,22 @@ class NCEdge extends UNISYS.Component { SetPermissions(data) { const { id } = this.state; const edgeIsLocked = data.lockedEdges.includes(id); - this.setState( - { - uIsLockedByDB: edgeIsLocked, - uIsLockedByTemplate: data.templateBeingEdited, - uIsLockedByImport: data.importActive - }, - () => this.UpdatePermissions() - ); + + // skip updates if there are no changes in values to optimize renders + const newState = { + uIsLockedByDB: edgeIsLocked, + uIsLockedByTemplate: data.templateBeingEdited, + uIsLockedByImport: data.importActive + }; + if ( + newState.uIsLockedByDB === this.state.uIsLockedByDB && + newState.uIsLockedByTemplate === this.state.uIsLockedByTemplate && + newState.uIsLockedByImport === this.state.uIsLockedByImport + ) { + return; + } + + this.setState(newState, () => this.UpdatePermissions()); } UpdatePermissions() { const { uIsLockedByDB, uIsLockedByTemplate, uIsLockedByImport } = this.state; @@ -817,8 +825,7 @@ class NCEdge extends UNISYS.Component { UIDisableEditMode() { this.UnlockEdge(() => { this.setState({ - uViewMode: NCUI.VIEWMODE.VIEW, - uIsLockedByDB: false + uViewMode: NCUI.VIEWMODE.VIEW }); // Clear the secondary selection @@ -940,7 +947,7 @@ class NCEdge extends UNISYS.Component {
EDGE {id}
- +
{NCUI.RenderLabel('source', defs['source'].displayLabel)} diff --git a/app/view/netcreate/components/NCEdgeTable.jsx b/app/view/netcreate/components/NCEdgeTable.jsx index d4d8a0948..f2917dead 100644 --- a/app/view/netcreate/components/NCEdgeTable.jsx +++ b/app/view/netcreate/components/NCEdgeTable.jsx @@ -4,227 +4,96 @@ EdgeTable is used to to display a table of edges for review. - It displays NCDATA. - But also read FILTEREDNCDATA to show highlight/filtered state + It checks FILTEREDNCDATA to show highlight/filtered state + It uses URTable for rendering and sorting. ## PROPS * tableHeight -- sets height based on InfoPanel dragger * isOpen -- whether the table is visible - ## TO USE EdgeTable is self contained and relies on global NCDATA to load. - - Set `DBG` to true to show the `ID` column. - - ## 2018-12-07 Update - - Since we're not using tab navigation: - 1. The table isExpanded is now true by default. - 2. The "Show/Hide Table" button is hidden. - - Reset these to restore previous behavior. - \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ -const React = require('react'); -const NCUI = require('../nc-ui'); -const CMTMGR = require('../comment-mgr'); -const FILTER = require('./filter/FilterEnums'); -const UNISYS = require('unisys/client'); -import HDATE from 'system/util/hdate'; -import URCommentVBtn from './URCommentVBtn'; +import React, { useState, useEffect } from 'react'; +import UNISYS from 'unisys/client'; +import NCUI from '../nc-ui'; +import UTILS from '../nc-utils'; +import FILTER from './filter/FilterEnums'; +import CMTMGR from '../comment-mgr'; + import URTable from './URTable'; -const { BUILTIN_FIELDS_EDGE } = require('system/util/enum'); -const { ICON_PENCIL, ICON_VIEW } = require('system/util/constant'); +import URCommentVBtn from './URCommentVBtn'; + +import { BUILTIN_FIELDS_EDGE } from 'system/util/enum'; +import { ICON_PENCIL, ICON_VIEW } from 'system/util/constant'; /// CONSTANTS & DECLARATIONS ////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -const DBG = false; -var UDATA = null; +/// Initialize UNISYS DATA LINK for functional react component +const UDATAOwner = { name: 'NCEdgeTable' }; +const UDATA = UNISYS.NewDataLink(UDATAOwner); -/// UTILITY METHODS /////////////////////////////////////////////////////////// -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -function u_GetButtonId(cref) { - return `table-comment-button-${cref}`; -} +const DBG = false; -/// REACT COMPONENT /////////////////////////////////////////////////////////// +/// REACT FUNCTIONAL COMPONENT //////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/// export a class object for consumption by brunch/require -class NCEdgeTable extends UNISYS.Component { - constructor(props) { - super(props); +function NCEdgeTable({ tableHeight, isOpen }) { + const [state, setState] = useState({}); - const TEMPLATE = this.AppState('TEMPLATE'); - this.state = { + /// USEEFFECT /////////////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + useEffect(() => { + const TEMPLATE = UDATA.AppState('TEMPLATE'); + const SESSION = UDATA.AppState('SESSION'); + setState({ edgeDefs: TEMPLATE.edgeDefs, edges: [], - selectedEdgeId: undefined, - selectedEdgeColor: TEMPLATE.sourceColor, nodes: [], // needed for dereferencing source/target disableEdit: false, - isLocked: false, - isExpanded: true, - sortkey: 'Relationship', - dummy: 0, // used to force render update + isLocked: !SESSION.isValid + }); - COLUMNDEFS: [] + UDATA.OnAppStateChange('FILTEREDNCDATA', urstate_FILTEREDNCDATA); + UDATA.OnAppStateChange('SESSION', urstate_SESSION); + UDATA.OnAppStateChange('TEMPLATE', urstate_TEMPLATE); + return () => { + UDATA.AppStateChangeOff('FILTEREDNCDATA', urstate_FILTEREDNCDATA); + UDATA.AppStateChangeOff('SESSION', urstate_SESSION); + UDATA.AppStateChangeOff('TEMPLATE', urstate_TEMPLATE); }; + }, []); - this.onUpdateCommentUI = this.onUpdateCommentUI.bind(this); - this.onStateChange_SESSION = this.onStateChange_SESSION.bind(this); - this.onStateChange_SELECTION = this.onStateChange_SELECTION.bind(this); - this.onEDGE_OPEN = this.onEDGE_OPEN.bind(this); - this.deriveFilteredEdges = this.deriveFilteredEdges.bind(this); - this.updateEdgeFilterState = this.updateEdgeFilterState.bind(this); - this.onStateChange_NCDATA = this.onStateChange_NCDATA.bind(this); - this.onStateChange_FILTEREDNCDATA = this.onStateChange_FILTEREDNCDATA.bind(this); - this.urmsg_EDIT_PERMISSIONS_UPDATE = - this.urmsg_EDIT_PERMISSIONS_UPDATE.bind(this); - this.onStateChange_TEMPLATE = this.onStateChange_TEMPLATE.bind(this); - this.onViewButtonClick = this.onViewButtonClick.bind(this); - this.onEditButtonClick = this.onEditButtonClick.bind(this); - this.onToggleExpanded = this.onToggleExpanded.bind(this); - this.onHighlightNode = this.onHighlightNode.bind(this); - this.m_FindMatchingObjsByProp = this.m_FindMatchingObjsByProp.bind(this); - this.m_FindMatchingEdgeByProp = this.m_FindMatchingEdgeByProp.bind(this); - this.m_FindEdgeById = this.m_FindEdgeById.bind(this); - this.lookupNodeLabel = this.lookupNodeLabel.bind(this); - - this.SetColumnDefs = this.SetColumnDefs.bind(this); - - this.sortDirection = 1; - - /// Initialize UNISYS DATA LINK for REACT - UDATA = UNISYS.NewDataLink(this); - - UDATA.HandleMessage('EDGE_OPEN', this.onEDGE_OPEN); - UDATA.HandleMessage( - 'EDIT_PERMISSIONS_UPDATE', - this.urmsg_EDIT_PERMISSIONS_UPDATE - ); - - // SESSION is called by SessionSHell when the ID changes - // set system-wide. data: { classId, projId, hashedId, groupId, isValid } - this.OnAppStateChange('SESSION', this.onStateChange_SESSION); - - // Always make sure class methods are bind()'d before using them - // as a handler, otherwise object context is lost - this.OnAppStateChange('NCDATA', this.onStateChange_NCDATA); - - // Handle Template updates - this.OnAppStateChange('TEMPLATE', this.onStateChange_TEMPLATE); - - // Track Filtered Data Updates too - this.OnAppStateChange('FILTEREDNCDATA', this.onStateChange_FILTEREDNCDATA); - - this.OnAppStateChange('SELECTION', this.onStateChange_SELECTION); - - // Comment Message Handlers - // Force update whenever threads are opened or closed - UDATA.HandleMessage('CTHREADMGR_THREAD_OPENED', this.onUpdateCommentUI); - UDATA.HandleMessage('CTHREADMGR_THREAD_CLOSED', this.onUpdateCommentUI); - UDATA.HandleMessage('CTHREADMGR_THREAD_CLOSED_ALL', this.onUpdateCommentUI); - } // constructor - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - componentDidMount() { - if (DBG) console.log('EdgeTable.componentDidMount!'); - - this.onStateChange_SESSION(this.AppState('SESSION')); - - // Explicitly retrieve data because we may not have gotten a NCDATA - // update while we were hidden. - let NCDATA = this.AppState('NCDATA'); - this.onStateChange_NCDATA(NCDATA); - - const COLUMNDEFS = this.SetColumnDefs(); - this.setState({ COLUMNDEFS }); - } - - componentWillUnmount() { - UDATA.UnhandleMessage('EDGE_OPEN', this.onEDGE_OPEN); - UDATA.UnhandleMessage( - 'EDIT_PERMISSIONS_UPDATE', - this.urmsg_EDIT_PERMISSIONS_UPDATE - ); - this.AppStateChangeOff('SESSION', this.onStateChange_SESSION); - this.AppStateChangeOff('NCDATA', this.onStateChange_NCDATA); - this.AppStateChangeOff('FILTEREDNCDATA', this.onStateChange_FILTEREDNCDATA); - this.AppStateChangeOff('TEMPLATE', this.onStateChange_TEMPLATE); - this.AppStateChangeOff('SELECTION', this.onStateChange_SELECTION); - UDATA.UnhandleMessage('CTHREADMGR_THREAD_OPENED', this.onUpdateCommentUI); - UDATA.UnhandleMessage('CTHREADMGR_THREAD_CLOSED', this.onUpdateCommentUI); - UDATA.UnhandleMessage('CTHREADMGR_THREAD_CLOSED_ALL', this.onUpdateCommentUI); - } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Force update so that URCommentVBtn selection state is updated - onUpdateCommentUI(data) { - this.setState({ dummy: this.state.dummy + 1 }); - } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - onStateChange_SELECTION(data) { - if (data === undefined) return; - - const selectedEdgeId = data.edges.length > 0 ? data.edges[0].id : undefined; - if (selectedEdgeId === this.state.selectedEdgeId) { - return; - } - this.setState({ selectedEdgeId }); - } - /** Handle change in SESSION data - Called both by componentWillMount() and AppStateChange handler. - The 'SESSION' state change is triggered in two places in SessionShell during - its handleChange() when active typing is occuring, and also during - SessionShell.componentWillMount() - */ - onStateChange_SESSION(decoded) { - const isLocked = !decoded.isValid; - if (isLocked === this.state.isLocked) { - return; + /// UR HANDLERS ///////////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + function urstate_FILTEREDNCDATA(data) { + if (data.edges) { + // If we're transitioning from "COLLAPSE" or "FOCUS" to "HILIGHT/FADE", then we + // also need to add back in edges that are not in filteredEdges + // (because "COLLAPSE" and "FOCUS" removes edges that are not matched) + const FILTERDEFS = UDATA.AppState('FILTERDEFS'); + if (FILTERDEFS.filterAction === FILTER.ACTION.FADE) { + const NCDATA = UDATA.AppState('NCDATA'); + m_updateEdgeFilterState(NCDATA.edges); + } else { + m_updateEdgeFilterState(data.edges); + } } - this.setState({ isLocked }); } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - displayUpdated(nodeEdge) { - // Prevent error if `meta` info is not defined yet, or not properly imported - if (!nodeEdge.meta) return ''; - - var d = new Date( - nodeEdge.meta.revision > 0 ? nodeEdge.meta.updated : nodeEdge.meta.created - ); - - var year = String(d.getFullYear()); - var date = d.getMonth() + 1 + '/' + d.getDate() + '/' + year.substr(2, 4); - var time = d.toTimeString().substr(0, 5); - var dateTime = date + ' at ' + time; - var titleString = 'v' + nodeEdge.meta.revision; - if (nodeEdge._nlog) - titleString += ' by ' + nodeEdge._nlog[nodeEdge._nlog.length - 1]; - var tag = {dateTime} ; - - return tag; + function m_updateEdgeFilterState(edges) { + const filteredEdges = m_deriveFilteredEdges(edges); + setState(prevState => ({ ...prevState, edges: filteredEdges })); } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// User selected edge usu by clicking NCNode's edge item in Edges tab - onEDGE_OPEN(data) { - this.setState({ selectedEdgeId: data.edge.id }); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Set node filtered status based on current filteredNodes - deriveFilteredEdges(edges) { + function m_deriveFilteredEdges(edges) { // set filter status let filteredEdges = []; // If we're transitioning from "HILIGHT/FADE" to "COLLAPSE" or "FOCUS", then we @@ -241,211 +110,46 @@ class NCEdgeTable extends UNISYS.Component { }); } else { // Fade - // Fading is handled by setting node.filteredTransparency which is + // Fading is handled by setting edge.filteredTransparency which is // directly handled by the filter now. So no need to process it // here in the table. filteredEdges = edges; } - + // } return filteredEdges; } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Set edge filtered status based on current filteredNodes - updateEdgeFilterState(edges) { - const filteredEdges = this.deriveFilteredEdges(edges); - this.setState({ edges: filteredEdges }); - } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Handle updated SELECTION: NCDATA updates - */ - onStateChange_NCDATA(data) { - if (data && data.edges && data.nodes) { - // NCDATA.edges no longer uses source/target objects - // ...1. So we need to save nodes for dereferencing. - this.setState({ nodes: data.nodes }, () => { - // ...2. So we stuff 'sourceLabel' and 'targetLabel' into the local edges array - let edges = data.edges.map(e => { - e.sourceLabel = this.lookupNodeLabel(e.source); // requires `state.nodes` be set - e.targetLabel = this.lookupNodeLabel(e.target); - return e; - }); - this.setState({ edges }); - const { filteredEdges } = this.state; - this.updateEdgeFilterState(edges, filteredEdges); - }); - } - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Handle FILTEREDNCDATA updates sent by filters-logic.m_FiltersApply - Note that edge.soourceLabel and edge.targetLabel should already be set - by filter-mgr. - */ - onStateChange_FILTEREDNCDATA(data) { - if (data.edges) { - const filteredEdges = data.edges; - // If we're transitioning from "COLLAPSE" or "FOCUS" to "HILIGHT/FADE", then we - // also need to add back in edges that are not in filteredEdges - // (because "COLLAPSE" and "FOCUS" removes edges that are not matched) - const FILTERDEFS = UDATA.AppState('FILTERDEFS'); - if (FILTERDEFS.filterAction === FILTER.ACTION.FADE) { - const NCDATA = UDATA.AppState('NCDATA'); - this.setState( - { - edges: NCDATA.edges, - filteredEdges - }, - () => { - const edges = NCDATA.edges; - this.updateEdgeFilterState(edges, filteredEdges); - } - ); - } else { - this.setState( - { - edges: filteredEdges, - filteredEdges - }, - () => { - const edges = filteredEdges; - this.updateEdgeFilterState(edges, filteredEdges); - } - ); - } + function urstate_SESSION(decoded) { + const isLocked = !decoded.isValid; + if (isLocked === this.state.isLocked) { + return; } + setState(prevState => ({ ...prevState, isLocked })); } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - urmsg_EDIT_PERMISSIONS_UPDATE() { - // Update COLUMNDEFS after permissions update so disabledState is shown - const cb = () => { - // Update COLUMNDEFS to update buttons - const COLUMNDEFS = this.SetColumnDefs(); - this.setState({ COLUMNDEFS }); - }; - this.updateEditState(cb); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Disable edit if someone else is editing a template - updateEditState(cb) { - // disable edit if someone else is editing a template, node, or edge - let disableEdit = false; - UDATA.NetCall('SRV_GET_EDIT_STATUS').then(data => { - // someone else might be editing a template or importing or editing node or edge - disableEdit = data.templateBeingEdited || data.importActive; - // REVIEW: Only disableEdit if template is being updated, otherwise allow edits - // || data.nodeOrEdgeBeingEdited || - // REVIEW: commentBeingEditedByMe shouldn't affect table? - // data.commentBeingEditedByMe; // only lock out if this user is the one editing comments, allow network commen edits - - // optimize, skip render if no change - if (disableEdit === this.state.disableEdit) { - return; - } - - this.setState({ disableEdit }, () => { - if (typeof cb === 'function') cb(); - }); - }); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - onStateChange_TEMPLATE(data) { - const COLUMNDEFS = this.SetColumnDefs(data.edgeDefs); - this.setState({ + function urstate_TEMPLATE(data) { + setState(prevState => ({ + ...prevState, edgeDefs: data.edgeDefs, - selectedEdgeColor: data.sourceColor, - COLUMNDEFS - }); - } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Look up the Node label for source / target ids - */ - lookupNodeLabel(nodeId) { - const node = this.state.nodes.find(n => n.id === nodeId); - if (node === undefined) return '...'; - // if (node === undefined) throw new Error('EdgeTable: Could not find node', nodeId); - return node.label; - } - - /// UI EVENT HANDLERS ///////////////////////////////////////////////////////// - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - onViewButtonClick(event, edgeId) { - event.preventDefault(); - event.stopPropagation(); - let edgeID = parseInt(edgeId); - let edge = this.m_FindEdgeById(edgeID); - if (DBG) console.log('EdgeTable: Edge id', edge.id, 'selected for viewing'); - // Load Source Node then Edge - UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [edge.source] }).then(() => { - UDATA.LocalCall('EDGE_SELECT', { edgeId: edge.id }); - }); - } - onEditButtonClick(event, edgeId) { - event.preventDefault(); - event.stopPropagation(); - let edgeID = parseInt(edgeId); - let edge = this.m_FindEdgeById(edgeID); - if (DBG) console.log('EdgeTable: Edge id', edge.id, 'selected for editing'); - // Load Source Node then Edge - UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [edge.source] }).then(() => { - UDATA.LocalCall('EDGE_SELECT_AND_EDIT', { edgeId: edge.id }); - }); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - onToggleExpanded(event) { - this.setState({ - isExpanded: !this.state.isExpanded - }); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - /*/ - onHighlightNode(nodeId) { - UDATA.LocalCall('TABLE_HILITE_NODE', { nodeId }); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ - */ - setSortKey(key, type) { - if (key === this.state.sortkey) this.sortDirection = -1 * this.sortDirection; - // if this was already the key, flip the direction - else this.sortDirection = 1; - - const edges = this.sortTable(key, this.state.edges, type); - this.setState({ - edges, - sortkey: key - }); - UNISYS.Log('sort edge table', key, this.sortDirection); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - selectNode(id, event) { - event.preventDefault(); - - // Load Source - if (DBG) console.log('EdgeTable: Edge id', id, 'selected for editing'); - UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [id] }); + selectedEdgeColor: data.sourceColor + })); } - /// URTABLE COLUMN DEFS ///////////////////////////////////////////////////// + /// COLUMN DEFINTION GENERATION ///////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SetColumnDefs(incomingEdgeDefs) { - const { edges, edgeDefs, disableEdit, isLocked } = this.state; - + function DeriveColumnDefs(incomingEdgeDefs) { + const { edges, edgeDefs, disableEdit, isLocked } = state; const defs = incomingEdgeDefs || edgeDefs; - let attributeDefs = Object.keys(defs).filter( - k => !BUILTIN_FIELDS_EDGE.includes(k) && !defs[k].hidden + // Only include built in fields + // Only include non-hidden fields + // Only include non-provenance fields + const attributeDefs = Object.keys(defs).filter( + k => + !BUILTIN_FIELDS_EDGE.includes(k) && !defs[k].isProvenance && !defs[k].hidden + ); + const provenanceDefs = Object.keys(defs).filter( + k => !BUILTIN_FIELDS_EDGE.includes(k) && defs[k].isProvenance && !defs[k].hidden ); /// CLICK HANDLERS @@ -475,16 +179,10 @@ class NCEdgeTable extends UNISYS.Component { event.stopPropagation(); UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [parseInt(nodeId)] }); } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Toggle Comment Button on and off - function ui_ClickComment(cref) { - const position = CMTMGR.GetCommentThreadPosition(u_GetButtonId(cref)); - const uiref = u_GetButtonId(cref); - CMTMGR.ToggleCommentCollection(uiref, cref, position); - } /// RENDERERS /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function RenderViewOrEdit(value) { + function col_RenderViewOrEdit(key, tdata, coldef) { + const value = tdata[key]; return (
{!disableEdit && ( @@ -511,46 +209,42 @@ class NCEdgeTable extends UNISYS.Component { // id: String; // label: String; // } - function RenderNode(value) { + function col_RenderNode(key, tdata, coldef) { + const value = tdata[key]; if (!value) return; // skip if not defined yet - if (value.id === undefined || value.label === undefined) { - // During Edge creation, source/target may not be defined yet - return ...; - } + if (tdata.id === undefined) + throw new Error(`RenderNode: id is undefined. tdata=${tdata}`); + if (value === undefined) + throw new Error(`RenderNode: label is undefined. value=${value}`); return ( ); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function RenderCommentBtn(value) { - return ( - ui_ClickComment(value.cref)} - /> - ); + function col_RenderCommentBtn(key, tdata, coldef) { + const value = tdata[key]; + return ; } /// CUSTOM SORTERS /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// tdata = TTblNodeObject[] = { id: String, label: String } - function SortNodes(key, tdata, order) { + function col_SortNodes(key, tdata, order) { const sortedData = [...tdata].sort((a, b) => { - if (a[key].label < b[key].label) return order; - if (a[key].label > b[key].label) return order * -1; + if (String(a[key].label).toLowerCase() < String(b[key].label).toLowerCase()) + return order; + if (String(a[key].label).toLowerCase() > String(b[key].label).toLowerCase()) + return order * -1; return 0; }); return sortedData; } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function SortCommentsByCount(key, tdata, order) { + function col_SortCommentsByCount(key, tdata, order) { const sortedData = [...tdata].sort((a, b) => { if (a[key].count < b[key].count) return order; if (a[key].count > b[key].count) return order * -1; @@ -563,11 +257,16 @@ class NCEdgeTable extends UNISYS.Component { // column definitions for custom attributes // (built in columns are: view, degrees, label) const ATTRIBUTE_COLUMNDEFS = attributeDefs.map(key => { - const title = defs[key].displayLabel; - const type = defs[key].type; return { - title, - type, + title: defs[key].displayLabel, + type: defs[key].type, + data: key + }; + }); + const PROVENANCE_COLUMNDEFS = provenanceDefs.map(key => { + return { + title: defs[key].displayLabel, + type: defs[key].type, data: key }; }); @@ -575,22 +274,24 @@ class NCEdgeTable extends UNISYS.Component { { title: '', // View/Edit data: 'id', - type: 'number', - width: 50, // in px - renderer: RenderViewOrEdit + type: defs['id'].type, + width: 45, // in px + renderer: col_RenderViewOrEdit, + sortDisabled: true, + tipDisabled: true }, { title: defs['source'].displayLabel, width: 130, // in px data: 'sourceDef', - renderer: RenderNode, - sorter: SortNodes + renderer: col_RenderNode, + sorter: col_SortNodes } ]; if (defs['type'] && !defs['type'].hidden) { COLUMNDEFS.push({ title: defs['type'].displayLabel, - type: 'text', + type: 'text-case-insensitive', width: 130, // in px data: 'type' }); @@ -600,91 +301,71 @@ class NCEdgeTable extends UNISYS.Component { title: defs['target'].displayLabel, width: 130, // in px data: 'targetDef', - renderer: RenderNode, - sorter: SortNodes + renderer: col_RenderNode, + sorter: col_SortNodes }, ...ATTRIBUTE_COLUMNDEFS, - { - title: 'Comments', - data: 'commentVBtnDef', - type: 'text', - width: 50, // in px - renderer: RenderCommentBtn, - sorter: SortCommentsByCount - } + ...PROVENANCE_COLUMNDEFS ); - - return COLUMNDEFS; - } - - /// OBJECT HELPERS //////////////////////////////////////////////////////////// - /// these probably should go into a utility class - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Return array of objects that match the match_me object keys/values - NOTE: make sure that strings are compared with strings, etc - */ - m_FindMatchingObjsByProp(obj_list, match_me = {}) { - // operate on arrays only - if (!Array.isArray(obj_list)) - throw Error('FindMatchingObjectsByProp arg1 must be array'); - let matches = obj_list.filter(obj => { - let pass = true; - for (let key in match_me) { - if (match_me[key] !== obj[key]) pass = false; - break; - } - return pass; + // History + if (defs['createdBy'] && !defs['createdBy'].hidden) + COLUMNDEFS.push({ + title: defs['createdBy'].displayLabel, + type: 'text-case-insensitive', + width: 60, // in px + data: 'createdBy' + }); + if (defs['created'] && !defs['created'].hidden) + COLUMNDEFS.push({ + title: defs['created'].displayLabel, + type: 'timestamp-short', + width: 60, // in px + data: 'created' + }); + if (defs['updatedBy'] && !defs['updatedBy'].hidden) + COLUMNDEFS.push({ + title: defs['updatedBy'].displayLabel, + type: 'text-case-insensitive', + width: 60, // in px + data: 'updatedBy' + }); + if (defs['updated'] && !defs['updated'].hidden) + COLUMNDEFS.push({ + title: defs['updated'].displayLabel, + type: 'timestamp-short', + width: 60, // in px + data: 'updated' + }); // Comment is last + COLUMNDEFS.push({ + title: ' ', + data: 'commentVBtnDef', + type: 'text', + width: 40, // in px + renderer: col_RenderCommentBtn, + sorter: col_SortCommentsByCount, + tipDisabled: true }); - // return array of matches (can be empty array) - return matches; - } - - /// EDGE HELPERS ////////////////////////////////////////////////////////////// - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Return array of nodes that match the match_me object keys/values - NOTE: make sure that strings are compared with strings, etc - */ - m_FindMatchingEdgeByProp(match_me = {}) { - return this.m_FindMatchingObjsByProp(this.state.edges, match_me); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Convenience function to retrieve edge by ID - */ - m_FindEdgeById(id) { - return this.m_FindMatchingEdgeByProp({ id })[0]; + return COLUMNDEFS; } - /// REACT LIFECYCLE METHODS /////////////////////////////////////////////////// - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** This is not yet implemented as of React 16.2. It's implemented in 16.3. - getDerivedStateFromProps (props, state) { - console.error('getDerivedStateFromProps!!!'); - } - */ - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - render() { - const { edges, edgeDefs, disableEdit, isLocked, COLUMNDEFS } = this.state; - const { isOpen, tableHeight } = this.props; - const uid = CMTMGR.GetCurrentUserId(); - + /// TABLE DATA GENERATION /////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + function DeriveTableData({ edgeDefs, edges }) { // Only include built in fields // Only include non-hidden fields + // Only include non-provenance fields let attributeDefs = Object.keys(edgeDefs).filter( - k => !BUILTIN_FIELDS_EDGE.includes(k) && !edgeDefs[k].hidden + k => + !BUILTIN_FIELDS_EDGE.includes(k) && + !edgeDefs[k].hidden && + !edgeDefs[k].isProvenance + ); + const provenanceDefs = Object.keys(edgeDefs).filter( + k => edgeDefs[k].isProvenance ); - // show 'type' between 'source' and 'target' if `type` has been defined - // if it isn't defined, just show attribute fields after `source` and 'target` - const hasTypeField = edgeDefs['type']; - if (hasTypeField) attributeDefs = attributeDefs.filter(a => a !== 'type'); - - /// TABLE DATA GENERATION - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const TABLEDATA = edges.map((edge, i) => { + return edges.map((edge, i) => { const { id, source, target, sourceLabel, targetLabel, type } = edge; - const sourceDef = { id: source, label: sourceLabel }; const targetDef = { id: target, label: targetLabel }; @@ -698,15 +379,23 @@ class NCEdgeTable extends UNISYS.Component { // b. provide the HTML string data.html = NCUI.Markdownify(edge[key]); data.raw = edge[key]; - } else if (edgeDefs[key].type === 'hdate') + } else if (edgeDefs[key].type === 'hdate') { data = edge[key] && edge[key].formattedDateString; - else data = edge[key]; + } else if (edgeDefs[key].type === 'infoOrigin') { + data = + edge[key] === undefined || edge[key] === '' + ? UTILS.DeriveInfoOriginString( + edge.createdBy, + edge.meta ? edge.meta.created : '' + ) + : edge[key]; + } else data = edge[key]; attributes[key] = data; }); // comment button definition - const cref = CMTMGR.GetNodeCREF(id); - const commentCount = CMTMGR.GetThreadedViewObjectsCount(cref, uid); + const cref = CMTMGR.GetEdgeCREF(id); + const commentCount = CMTMGR.GetCommentCollectionCount(cref); const ccol = CMTMGR.GetCommentCollection(cref) || {}; const hasUnreadComments = ccol.hasUnreadComments; const selected = CMTMGR.GetOpenComments(cref); @@ -717,6 +406,38 @@ class NCEdgeTable extends UNISYS.Component { selected }; + // provenance + const provenance = {}; + provenanceDefs.forEach((key, i) => { + let data = {}; + if (edgeDefs[key].type === 'markdown') { + // for markdown: + // a. provide the raw markdown string + // b. provide the HTML string + data.html = NCUI.Markdownify(edge[key]); + data.raw = edge[key]; + } else if (edgeDefs[key].type === 'hdate') { + data = edge[key] && edge[key].formattedDateString; + } else if (edgeDefs[key].type === 'infoOrigin') { + data = + edge[key] === undefined || edge[key] === '' + ? UTILS.DeriveInfoOriginString( + edge.createdBy, + edge.meta ? edge.meta.created : '' + ) + : edge[key]; + } else data = edge[key] || ''; + provenance[key] = data; + }); + + // history + const history = { + createdBy: edge.createdBy, + created: edge.meta ? edge.meta.created : '', // meta may not be defined when a new node is creatd + updatedBy: edge.updatedBy, + updated: edge.meta ? edge.meta.updated : '' // meta may not be defined when a new node is creatd + }; + return { id: { edgeId: id, sourceId: source }, // { edgeId, sourceId} for click handler sourceDef, // { id: String, label: String } @@ -724,19 +445,29 @@ class NCEdgeTable extends UNISYS.Component { type, ...attributes, commentVBtnDef, + ...provenance, + ...history, meta: { filteredTransparency: edge.filteredTransparency } }; }); - return ( -
- -
- ); } -} // class EdgeTable + + /// COMPONENT RENDER //////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + if (state.edges === undefined) return `loading...waiting for edges ${state.edges}`; + if (state.edgeDefs === undefined) + return `loading...waiting for nodeDefs ${state.edgeDefs}`; + const COLUMNDEFS = DeriveColumnDefs(); + const TABLEDATA = DeriveTableData({ edgeDefs: state.edgeDefs, edges: state.edges }); + return ( +
+ +
+ ); +} /// EXPORT REACT COMPONENT //////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -module.exports = NCEdgeTable; +export default NCEdgeTable; diff --git a/app/view/netcreate/components/NCNode.css b/app/view/netcreate/components/NCNode.css index 9d22c908a..924e7fee7 100644 --- a/app/view/netcreate/components/NCNode.css +++ b/app/view/netcreate/components/NCNode.css @@ -234,11 +234,6 @@ .nccomponent .formview .viewvalue img { max-width: 225px; } - -.URTable img { - max-width: 225px; -} - .nccomponent .formview div.targetarrow { margin-top: -5px; text-align: center; diff --git a/app/view/netcreate/components/NCNode.jsx b/app/view/netcreate/components/NCNode.jsx index 98317aa1c..881bfeb69 100644 --- a/app/view/netcreate/components/NCNode.jsx +++ b/app/view/netcreate/components/NCNode.jsx @@ -48,7 +48,7 @@ const NCUI = require('../nc-ui'); const NCEdge = require('./NCEdge'); const NCDialogCitation = require('./NCDialogCitation'); const SETTINGS = require('settings'); -import URCommentBtn from './URCommentBtn'; +import URCommentVBtn from './URCommentVBtn'; /// CONSTANTS & DECLARATIONS ////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -170,8 +170,8 @@ class NCNode extends UNISYS.Component { label: '', type: '', degrees: null, - attributes: [], - provenance: [], + attributes: {}, + provenance: {}, created: undefined, createdBy: undefined, updated: undefined, @@ -248,15 +248,23 @@ class NCNode extends UNISYS.Component { // someone else might be editing a template or importing or editing node or edge const { id } = this.state; const nodeIsLocked = data.lockedNodes.includes(id); - this.setState( - { - uIsLockedByDB: nodeIsLocked, - uIsLockedByTemplate: data.templateBeingEdited, - uIsLockedByImport: data.importActive, - uIsLockedByComment: data.commentBeingEditedByMe - }, - () => this.UpdatePermissions() - ); + + // skip updates if there are no changes in values to optimize renders + const newState = { + uIsLockedByDB: nodeIsLocked, + uIsLockedByTemplate: data.templateBeingEdited, + uIsLockedByImport: data.importActive, + uIsLockedByComment: data.commentBeingEditedByMe + }; + if ( + newState.uIsLockedByDB === this.state.uIsLockedByDB && + newState.uIsLockedByTemplate === this.state.uIsLockedByTemplate && + newState.uIsLockedByImport === this.state.uIsLockedByImport && + newState.uIsLockedByComment === this.state.uIsLockedByComment + ) { + return; + } + this.setState(newState, () => this.UpdatePermissions()); }); } UpdatePermissions() { @@ -680,8 +688,7 @@ class NCNode extends UNISYS.Component { UIDisableEditMode() { this.UnlockNode(() => { this.setState({ - uViewMode: NCUI.VIEWMODE.VIEW, - uIsLockedByDB: false + uViewMode: NCUI.VIEWMODE.VIEW }); UDATA.NetCall('SRV_RELEASE_EDIT_LOCK', { editor: EDITORTYPE.NODE }); }); @@ -774,7 +781,7 @@ class NCNode extends UNISYS.Component {
NODE {id}
{NCUI.RenderLabel('label', label)}
- + {/* using key resets with a new URComment */}
{/* Special handling for `type` field */} diff --git a/app/view/netcreate/components/NCNodeTable.jsx b/app/view/netcreate/components/NCNodeTable.jsx index 1b47e48fb..00e2596e8 100644 --- a/app/view/netcreate/components/NCNodeTable.jsx +++ b/app/view/netcreate/components/NCNodeTable.jsx @@ -4,15 +4,9 @@ NCNodeTable is used to to display a table of nodes for review. - It displays NCDATA. - But also checks FILTEREDNCDATA to show highlight/filtered state - - This is intended to be a generic table implementation that enables - swapping in different table implementations. - - This is an abstraction of the original NodeTable component to make - it easier to swap in custom table components. + It checks FILTEREDNCDATA to show highlight/filtered state + It uses URTable for rendering and sorting. ## PROPS @@ -21,218 +15,87 @@ ## TO USE - NCNodeTable is self contained and relies on global NCDATA to load. + NCNodeTable is self contained and relies on global FILTEREDNCDATA to load. \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ -const React = require('react'); -const NCUI = require('../nc-ui'); -const CMTMGR = require('../comment-mgr'); -const FILTER = require('./filter/FilterEnums'); -const { BUILTIN_FIELDS_NODE } = require('system/util/enum'); -const UNISYS = require('unisys/client'); -import HDATE from 'system/util/hdate'; -import URCommentVBtn from './URCommentVBtn'; +import React, { useState, useEffect } from 'react'; +import UNISYS from 'unisys/client'; +import NCUI from '../nc-ui'; +import UTILS from '../nc-utils'; +import FILTER from './filter/FilterEnums'; +import CMTMGR from '../comment-mgr'; + import URTable from './URTable'; -const { ICON_PENCIL, ICON_VIEW } = require('system/util/constant'); +import URCommentVBtn from './URCommentVBtn'; + +import { BUILTIN_FIELDS_NODE } from 'system/util/enum'; +import { ICON_PENCIL, ICON_VIEW } from 'system/util/constant'; /// CONSTANTS & DECLARATIONS ////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -const DBG = false; -let UDATA = null; +/// Initialize UNISYS DATA LINK for functional react component +const UDATAOwner = { name: 'NCNodeTable' }; +const UDATA = UNISYS.NewDataLink(UDATAOwner); -/// UTILITY METHODS /////////////////////////////////////////////////////////// -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/// Comment Button ID based on cref -/// NOTE: This is different from the commenButtonID of URComentBtn, -/// which is simply `comment-button-${cref}` so that we can distinguish -/// clicks from the NCNodeTable from clicks from Node/Edges. -function u_GetButtonId(cref) { - return `table-comment-button-${cref}`; -} +const DBG = false; -/// REACT COMPONENT /////////////////////////////////////////////////////////// +/// REACT FUNCTIONAL COMPONENT //////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/// export a class object for consumption by brunch/require -class NCNodeTable extends UNISYS.Component { - constructor(props) { - super(props); +function NCNodeTable({ tableHeight, isOpen }) { + const [state, setState] = useState({}); - const TEMPLATE = this.AppState('TEMPLATE'); - this.state = { + /// USEEFFECT /////////////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + useEffect(() => { + const TEMPLATE = UDATA.AppState('TEMPLATE'); + const SESSION = UDATA.AppState('SESSION'); + setState({ nodeDefs: TEMPLATE.nodeDefs, nodes: [], - selectedNodeId: undefined, - hilitedNodeId: undefined, - selectedNodeColor: TEMPLATE.sourceColor, - hilitedNodeColor: TEMPLATE.searchColor, disableEdit: false, - isLocked: false, - isExpanded: true, - sortkey: 'label', - dummy: 0, // used to force render update + isLocked: !SESSION.isValid + }); - COLUMNDEFS: [] + UDATA.OnAppStateChange('FILTEREDNCDATA', urstate_FILTEREDNCDATA); + UDATA.OnAppStateChange('SESSION', urstate_SESSION); + UDATA.OnAppStateChange('TEMPLATE', urstate_TEMPLATE); + return () => { + UDATA.AppStateChangeOff('FILTEREDNCDATA', urstate_FILTEREDNCDATA); + UDATA.AppStateChangeOff('SESSION', urstate_SESSION); + UDATA.AppStateChangeOff('TEMPLATE', urstate_TEMPLATE); }; + }, []); - this.onUpdateCommentUI = this.onUpdateCommentUI.bind(this); - this.onStateChange_SESSION = this.onStateChange_SESSION.bind(this); - this.onStateChange_SELECTION = this.onStateChange_SELECTION.bind(this); - this.onStateChange_HILITE = this.onStateChange_HILITE.bind(this); - this.displayUpdated = this.displayUpdated.bind(this); - this.deriveFilteredNodes = this.deriveFilteredNodes.bind(this); - this.updateNodeFilterState = this.updateNodeFilterState.bind(this); - this.urmsg_EDIT_PERMISSIONS_UPDATE = - this.urmsg_EDIT_PERMISSIONS_UPDATE.bind(this); - this.updateEditState = this.updateEditState.bind(this); - this.onStateChange_NCDATA = this.onStateChange_NCDATA.bind(this); - this.onStateChange_FILTEREDNCDATA = this.onStateChange_FILTEREDNCDATA.bind(this); - this.onStateChange_TEMPLATE = this.onStateChange_TEMPLATE.bind(this); - this.onViewButtonClick = this.onViewButtonClick.bind(this); - this.onEditButtonClick = this.onEditButtonClick.bind(this); - this.onToggleExpanded = this.onToggleExpanded.bind(this); - this.onHighlightRow = this.onHighlightRow.bind(this); - - this.SetColumnDefs = this.SetColumnDefs.bind(this); - - /// Initialize UNISYS DATA LINK for REACT - UDATA = UNISYS.NewDataLink(this); - - UDATA.HandleMessage( - 'EDIT_PERMISSIONS_UPDATE', - this.urmsg_EDIT_PERMISSIONS_UPDATE - ); - - // SESSION is called by SessionSHell when the ID changes - // set system-wide. data: { classId, projId, hashedId, groupId, isValid } - this.OnAppStateChange('SESSION', this.onStateChange_SESSION); - - // Always make sure class methods are bind()'d before using them - // as a handler, otherwise object context is lost - this.OnAppStateChange('NCDATA', this.onStateChange_NCDATA); - - // Track Filtered Data Updates too - this.OnAppStateChange('FILTEREDNCDATA', this.onStateChange_FILTEREDNCDATA); - - // Handle Template updates - this.OnAppStateChange('TEMPLATE', this.onStateChange_TEMPLATE); - - this.OnAppStateChange('SELECTION', this.onStateChange_SELECTION); - this.OnAppStateChange('HILITE', this.onStateChange_HILITE); - - // Comment Message Handlers - // Force update whenever threads are opened or closed - UDATA.HandleMessage('CTHREADMGR_THREAD_OPENED', this.onUpdateCommentUI); - UDATA.HandleMessage('CTHREADMGR_THREAD_CLOSED', this.onUpdateCommentUI); - UDATA.HandleMessage('CTHREADMGR_THREAD_CLOSED_ALL', this.onUpdateCommentUI); - } // constructor - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - componentDidMount() { - if (DBG) console.log('NodeTable.componentDidMount!'); - this.onStateChange_SESSION(this.AppState('SESSION')); - - // Explicitly retrieve data because we may not have gotten a NCDATA - // update while we were hidden. - let NCDATA = this.AppState('NCDATA'); - this.onStateChange_NCDATA(NCDATA); - - const COLUMNDEFS = this.SetColumnDefs(); - this.setState({ COLUMNDEFS }); - } - - componentWillUnmount() { - UDATA.UnhandleMessage( - 'EDIT_PERMISSIONS_UPDATE', - this.urmsg_EDIT_PERMISSIONS_UPDATE - ); - this.AppStateChangeOff('SESSION', this.onStateChange_SESSION); - this.AppStateChangeOff('NCDATA', this.onStateChange_NCDATA); - this.AppStateChangeOff('FILTEREDNCDATA', this.onStateChange_FILTEREDNCDATA); - this.AppStateChangeOff('TEMPLATE', this.onStateChange_TEMPLATE); - this.AppStateChangeOff('SELECTION', this.onStateChange_SELECTION); - this.AppStateChangeOff('HILITE', this.onStateChange_HILITE); - UDATA.UnhandleMessage('CTHREADMGR_THREAD_OPENED', this.onUpdateCommentUI); - UDATA.UnhandleMessage('CTHREADMGR_THREAD_CLOSED', this.onUpdateCommentUI); - UDATA.UnhandleMessage('CTHREADMGR_THREAD_CLOSED_ALL', this.onUpdateCommentUI); - } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Force update so that URCommentVBtn selection state is updated - onUpdateCommentUI(data) { - this.setState({ dummy: this.state.dummy + 1 }); - } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - onStateChange_SELECTION(data) { - if (data === undefined) return; - - const selectedNodeId = data.nodes.length > 0 ? data.nodes[0].id : undefined; - if (selectedNodeId === this.state.selectedNodeId) { - return; - } - this.setState({ selectedNodeId }); - } - - onStateChange_HILITE(data) { - const { userHighlightNodeId, autosuggestHiliteNodeId } = data; // ignores `tableHiliteNodeId` - let hilitedNodeId; - if (autosuggestHiliteNodeId !== undefined) - hilitedNodeId = autosuggestHiliteNodeId; - if (userHighlightNodeId !== undefined) hilitedNodeId = userHighlightNodeId; - if (hilitedNodeId === this.state.hilitedNodeId) { - return; - } - this.setState({ hilitedNodeId }); - } - - /** Handle change in SESSION data - Called both by componentWillMount() and AppStateChange handler. - The 'SESSION' state change is triggered in two places in SessionShell during - its handleChange() when active typing is occuring, and also during - SessionShell.componentWillMount() - */ - onStateChange_SESSION(decoded) { - const isLocked = !decoded.isValid; - if (isLocked === this.state.isLocked) { - return; + /// UR HANDLERS ///////////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + function urstate_FILTEREDNCDATA(data) { + if (data.nodes) { + // If we're transitioning from "COLLAPSE" or "FOCUS" to "HILIGHT/FADE", then we + // also need to add back in nodes that are not in filteredNodes + // (because "COLLAPSE" and "FOCUS" removes nodes that are not matched) + const NCDATA = UDATA.AppState('NCDATA'); + const FILTERDEFS = UDATA.AppState('FILTERDEFS'); + if (FILTERDEFS.filterAction === FILTER.ACTION.FADE) { + // show ALL nodes + m_updateNodeFilterState(NCDATA.nodes); + } else { + // show only filtered nodes from the filter update + m_updateNodeFilterState(data.nodes); + } } - this.setState({ isLocked }); } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - displayUpdated(nodeEdge) { - // Prevent error if `meta` info is not defined yet, or not properly imported - - // this does not ever fire, revert! - console.error('NodeTable meta not defined yet', nodeEdge); - if (!nodeEdge.meta) { - return ''; - } - var d = new Date( - nodeEdge.meta.revision > 0 ? nodeEdge.meta.updated : nodeEdge.meta.created - ); - - var year = String(d.getFullYear()); - var date = d.getMonth() + 1 + '/' + d.getDate() + '/' + year.substr(2, 4); - var time = d.toTimeString().substr(0, 5); - var dateTime = date + ' at ' + time; - var titleString = 'v' + nodeEdge.meta.revision; - if (nodeEdge._nlog) - titleString += ' by ' + nodeEdge._nlog[nodeEdge._nlog.length - 1]; - var tag = {dateTime} ; - - return tag; + function m_updateNodeFilterState(nodes) { + const filteredNodes = m_deriveFilteredNodes(nodes); + setState(prevState => ({ ...prevState, nodes: filteredNodes })); } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Set node filtered status based on current filteredNodes - deriveFilteredNodes(nodes) { + function m_deriveFilteredNodes(nodes) { // set filter status let filteredNodes = []; // If we're transitioning from "HILIGHT/FADE" to "COLLAPSE" or "FOCUS", then we @@ -257,150 +120,39 @@ class NCNodeTable extends UNISYS.Component { // } return filteredNodes; } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Set node filtered status based on current filteredNodes - updateNodeFilterState(nodes) { - const filteredNodes = this.deriveFilteredNodes(nodes); - this.setState({ nodes: filteredNodes }); - } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Hide/Show "View" and "Edit" buttons in table based on template edit state - urmsg_EDIT_PERMISSIONS_UPDATE() { - // Update COLUMNDEFS after permissions update so disabledState is shown - const cb = () => { - // Update COLUMNDEFS to update buttons - const COLUMNDEFS = this.SetColumnDefs(); - this.setState({ COLUMNDEFS }); - }; - this.updateEditState(cb); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Disable edit if someone else is editing a template - updateEditState(cb) { - let disableEdit = false; - UDATA.NetCall('SRV_GET_EDIT_STATUS').then(data => { - // someone else might be editing a template or importing or editing node or edge - disableEdit = data.templateBeingEdited || data.importActive; - // REVIEW: Only disableEdit if template is being updated, otherwise allow edits - // || data.nodeOrEdgeBeingEdited || - // REVIEW: commentBeingEditedByMe shouldn't affect table? - // data.commentBeingEditedByMe; // only lock out if this user is the one editing comments, allow network commen edits - - // optimize, skip render if no change - if (disableEdit === this.state.disableEdit) { - return; - } - - this.setState({ disableEdit }, () => { - if (typeof cb === 'function') cb(); - }); - }); - } - - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** Handle updated SELECTION - */ - onStateChange_NCDATA(data) { - if (DBG) console.log('handle data update'); - if (data.nodes) { - const filteredNodes = this.deriveFilteredNodes(data.nodes); - // REVIEW DO SOMETHING. SELECTION update is not currently being handled. - } - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - onStateChange_FILTEREDNCDATA(data) { - if (data.nodes) { - // If we're transitioning from "COLLAPSE" or "FOCUS" to "HILIGHT/FADE", then we - // also need to add back in nodes that are not in filteredNodes - // (because "COLLAPSE" and "FOCUS" removes nodes that are not matched) - const NCDATA = UDATA.AppState('NCDATA'); - const FILTERDEFS = UDATA.AppState('FILTERDEFS'); - if (FILTERDEFS.filterAction === FILTER.ACTION.FADE) { - // show ALL nodes - this.updateNodeFilterState(NCDATA.nodes); - } else { - // show only filtered nodes from the filter update - this.updateNodeFilterState(data.nodes); - } + function urstate_SESSION(decoded) { + const isLocked = !decoded.isValid; + if (isLocked === this.state.isLocked) { + return; } + setState(prevState => ({ ...prevState, isLocked })); } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - onStateChange_TEMPLATE(data) { - const COLUMNDEFS = this.SetColumnDefs(data.nodeDefs); - this.setState({ + function urstate_TEMPLATE(data) { + setState(prevState => ({ + ...prevState, nodeDefs: data.nodeDefs, selectedNodeColor: data.sourceColor, - hilitedNodeColor: data.searchColor, - COLUMNDEFS - }); - } - - /// UTILITIES ///////////////////////////////////////////////////////////////// - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// UI EVENT HANDLERS ///////////////////////////////////////////////////////// - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - onViewButtonClick(event, nodeId) { - event.preventDefault(); - event.stopPropagation(); - let nodeID = parseInt(nodeId); - UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [nodeID] }); - } - /** - */ - onEditButtonClick(event, nodeId) { - event.preventDefault(); - event.stopPropagation(); - let nodeID = parseInt(nodeId); - UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [nodeID] }).then(() => { - if (DBG) console.error('NodeTable: Calling NODE_EDIT', nodeID); - UDATA.LocalCall('NODE_EDIT', { nodeID: nodeID }); - }); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - onToggleExpanded(event) { - this.setState({ - isExpanded: !this.state.isExpanded - }); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - onHighlightRow(nodeId) { - UDATA.LocalCall('TABLE_HILITE_NODE', { nodeId }); - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - */ - selectNode(id, event) { - event.preventDefault(); - - // REVIEW: For some reason React converts the integer IDs into string - // values when returned in event.target.value. So we have to convert - // it here. - // Load Source - UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [parseInt(id)] }); + hilitedNodeColor: data.searchColor + })); } - /// URTABLE COLUMN DEFS ///////////////////////////////////////////////////// + /// COLUMN DEFINTION GENERATION ///////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SetColumnDefs(incomingNodeDefs) { - const { nodeDefs, disableEdit, isLocked } = this.state; - + function DeriveColumnDefs(incomingNodeDefs) { + const { nodeDefs, disableEdit, isLocked } = state; const defs = incomingNodeDefs || nodeDefs; // Only include built in fields // Only include non-hidden fields + // Only include non-provenance fields const attributeDefs = Object.keys(defs).filter( - k => !BUILTIN_FIELDS_NODE.includes(k) && !defs[k].hidden + k => + !BUILTIN_FIELDS_NODE.includes(k) && !defs[k].isProvenance && !defs[k].hidden + ); + const provenanceDefs = Object.keys(defs).filter( + k => !BUILTIN_FIELDS_NODE.includes(k) && defs[k].isProvenance && !defs[k].hidden ); /// CLICK HANDLERS @@ -420,15 +172,10 @@ class NCNodeTable extends UNISYS.Component { UDATA.LocalCall('NODE_EDIT', { nodeID: nodeID }); }); } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function ui_ClickCommentBtn(cref) { - const position = CMTMGR.GetCommentThreadPosition(u_GetButtonId(cref)); - const uiref = u_GetButtonId(cref); - CMTMGR.ToggleCommentCollection(uiref, cref, position); - } /// RENDERERS /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function RenderViewOrEdit(value) { + function col_RenderViewOrEdit(key, tdata, coldef) { + const value = tdata[key]; return (
{!disableEdit && ( @@ -455,46 +202,30 @@ class NCNodeTable extends UNISYS.Component { // id: String; // label: String; // } - function RenderNode(value) { + function col_RenderNode(key, tdata, coldef) { + const value = tdata[key]; if (!value) return; // skip if not defined yet - if (value.id === undefined) - throw new Error('RenderNode: value.id is undefined'); - if (value.label === undefined) - throw new Error('RenderNode: value.label is undefined'); + if (tdata.id === undefined) + throw new Error(`RenderNode: id is undefined. tdata=${tdata}`); + if (value === undefined) + throw new Error(`RenderNode: label is undefined. value=${value}`); return ( ); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function RenderCommentBtn(value) { - return ( - ui_ClickCommentBtn(value.cref)} - /> - ); + function col_RenderCommentBtn(key, tdata, coldef) { + const value = tdata[key]; + return ; } /// CUSTOM SORTERS /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// tdata = TTblNodeObject[] = { id: String, label: String } - function SortNodes(key, tdata, order) { - const sortedData = [...tdata].sort((a, b) => { - if (a[key].label < b[key].label) return order; - if (a[key].label > b[key].label) return order * -1; - return 0; - }); - return sortedData; - } - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function SortCommentsByCount(key, tdata, order) { + function col_SortCommentsByCount(key, tdata, order) { const sortedData = [...tdata].sort((a, b) => { if (a[key].count < b[key].count) return order; if (a[key].count > b[key].count) return order * -1; @@ -507,11 +238,16 @@ class NCNodeTable extends UNISYS.Component { // column definitions for custom attributes // (built in columns are: view, degrees, label) const ATTRIBUTE_COLUMNDEFS = attributeDefs.map(key => { - const title = defs[key].displayLabel; - const type = defs[key].type; return { - title, - type, + title: defs[key].displayLabel, + type: defs[key].type, + data: key + }; + }); + const PROVENANCE_COLUMNDEFS = provenanceDefs.map(key => { + return { + title: defs[key].displayLabel, + type: defs[key].type, data: key }; }); @@ -519,76 +255,96 @@ class NCNodeTable extends UNISYS.Component { { title: '', // View/Edit data: 'id', - type: 'number', + type: defs['id'].type, width: 45, // in px - renderer: RenderViewOrEdit + renderer: col_RenderViewOrEdit, + sortDisabled: true, + tipDisabled: true }, { title: defs['degrees'].displayLabel, - type: 'number', + type: defs['degrees'].type, width: 50, // in px data: 'degrees' }, { title: defs['label'].displayLabel, + type: 'text-case-insensitive', data: 'label', - width: 300, // in px - renderer: RenderNode, - sorter: SortNodes + width: 200, // in px + renderer: col_RenderNode } ]; if (defs['type'] && !defs['type'].hidden) { COLUMNDEFS.push({ title: defs['type'].displayLabel, - type: 'text', + type: 'text-case-insensitive', width: 130, // in px data: 'type' }); } - COLUMNDEFS.push(...ATTRIBUTE_COLUMNDEFS, { - title: 'Comments', + COLUMNDEFS.push(...ATTRIBUTE_COLUMNDEFS); + COLUMNDEFS.push(...PROVENANCE_COLUMNDEFS); + // History + if (defs['createdBy'] && !defs['createdBy'].hidden) + COLUMNDEFS.push({ + title: defs['createdBy'].displayLabel, + type: 'text-case-insensitive', + width: 60, // in px + data: 'createdBy' + }); + if (defs['created'] && !defs['created'].hidden) + COLUMNDEFS.push({ + title: defs['created'].displayLabel, + type: 'timestamp-short', + width: 60, // in px + data: 'created' + }); + if (defs['updatedBy'] && !defs['updatedBy'].hidden) + COLUMNDEFS.push({ + title: defs['updatedBy'].displayLabel, + type: 'text-case-insensitive', + width: 60, // in px + data: 'updatedBy' + }); + if (defs['updated'] && !defs['updated'].hidden) + COLUMNDEFS.push({ + title: defs['updated'].displayLabel, + type: 'timestamp-short', + width: 60, // in px + data: 'updated' + }); + // Comment is last + COLUMNDEFS.push({ + title: ' ', data: 'commentVBtnDef', - width: 50, // in px - renderer: RenderCommentBtn, - sorter: SortCommentsByCount + width: 40, // in px + renderer: col_RenderCommentBtn, + sorter: col_SortCommentsByCount, + tipDisabled: true }); return COLUMNDEFS; } - /// REACT LIFECYCLE METHODS ///////////////////////////////////////////////// + /// TABLE DATA GENERATION /////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - render() { - const { - nodes, - nodeDefs, - selectedNodeId, - hilitedNodeId, - selectedNodeColor, - hilitedNodeColor, - disableEdit, - isLocked, - COLUMNDEFS - } = this.state; - if (nodes === undefined) return ''; - const { isOpen, tableHeight } = this.props; - - // skip rendering if COLUMNDEFS is not defined yet - // This ensures that URTable is inited only AFTER data has been loaded. - if (COLUMNDEFS.length < 1) return ''; - - const uid = CMTMGR.GetCurrentUserId(); - + function DeriveTableData({ nodeDefs, nodes }) { + // Only include built in fields + // Only include non-hidden fields + // Only include non-provenance fields const attributeDefs = Object.keys(nodeDefs).filter( - k => !BUILTIN_FIELDS_NODE.includes(k) && !nodeDefs[k].hidden + k => + !BUILTIN_FIELDS_NODE.includes(k) && + !nodeDefs[k].hidden && + !nodeDefs[k].isProvenance + ); + const provenanceDefs = Object.keys(nodeDefs).filter( + k => nodeDefs[k].isProvenance ); - /// TABLE DATA GENERATION - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const TABLEDATA = nodes.map((node, i) => { + return nodes.map((node, i) => { const { id, label, type, degrees } = node; - const sourceDef = { id, label }; - // custom attributes const attributes = {}; attributeDefs.forEach((key, i) => { @@ -599,15 +355,23 @@ class NCNodeTable extends UNISYS.Component { // b. provide the HTML string data.html = NCUI.Markdownify(node[key]); data.raw = node[key]; - } else if (nodeDefs[key].type === 'hdate') + } else if (nodeDefs[key].type === 'hdate') { data = node[key] && node[key].formattedDateString; - else data = node[key]; + } else if (nodeDefs[key].type === 'infoOrigin') { + data = + node[key] === undefined || node[key] === '' + ? UTILS.DeriveInfoOriginString( + node.createdBy, + node.meta ? node.meta.created : '' + ) + : node[key]; + } else data = node[key]; attributes[key] = data; }); // comment button definition const cref = CMTMGR.GetNodeCREF(id); - const commentCount = CMTMGR.GetThreadedViewObjectsCount(cref, uid); + const commentCount = CMTMGR.GetCommentCollectionCount(cref); const ccol = CMTMGR.GetCommentCollection(cref) || {}; const hasUnreadComments = ccol.hasUnreadComments; const selected = CMTMGR.GetOpenComments(cref); @@ -618,27 +382,68 @@ class NCNodeTable extends UNISYS.Component { selected }; + // provenance + const provenance = {}; + provenanceDefs.forEach((key, i) => { + let data = {}; + if (nodeDefs[key].type === 'markdown') { + // for markdown: + // a. provide the raw markdown string + // b. provide the HTML string + data.html = NCUI.Markdownify(node[key]); + data.raw = node[key]; + } else if (nodeDefs[key].type === 'hdate') { + data = node[key] && node[key].formattedDateString; + } else if (nodeDefs[key].type === 'infoOrigin') { + data = + node[key] === undefined || node[key] === '' + ? UTILS.DeriveInfoOriginString( + node.createdBy, + node.meta ? node.meta.created : '' + ) + : node[key]; + } else data = node[key] || ''; + provenance[key] = data; + }); + + // history + const history = { + createdBy: node.createdBy, + created: node.meta ? node.meta.created : '', // meta may not be defined when a new node is creatd + updatedBy: node.updatedBy, + updated: node.meta ? node.meta.updated : '' // meta may not be defined when a new node is creatd + }; + return { id, - label: sourceDef, // { id, label } so that we can render a button + label, type, degrees, ...attributes, commentVBtnDef, + ...provenance, + ...history, meta: { filteredTransparency: node.filteredTransparency } }; }); - - return ( -
- -
- ); } + + /// COMPONENT RENDER //////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + if (state.nodes === undefined) return `loading...waiting for nodes ${state.nodes}`; + if (state.nodeDefs === undefined) + return `loading...waiting for nodeDefs ${state.nodeDefs}`; + const COLUMNDEFS = DeriveColumnDefs(); + const TABLEDATA = DeriveTableData({ nodeDefs: state.nodeDefs, nodes: state.nodes }); + return ( +
+ +
+ ); } /// EXPORT REACT COMPONENT //////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -module.exports = NCNodeTable; +export default NCNodeTable; diff --git a/app/view/netcreate/components/Template.jsx b/app/view/netcreate/components/Template.jsx index b66ddba84..b3cb069a6 100644 --- a/app/view/netcreate/components/Template.jsx +++ b/app/view/netcreate/components/Template.jsx @@ -35,7 +35,6 @@ const React = require('react'); const ReactStrap = require('reactstrap'); const { Button } = ReactStrap; -import { JSONEditor } from '@json-editor/json-editor'; const UNISYS = require('unisys/client'); const { EDITORTYPE } = require('system/util/enum'); const TEMPLATE_MGR = require('../templateEditor-mgr'); diff --git a/app/view/netcreate/components/URComment.css b/app/view/netcreate/components/URComment.css index ad68dcdf9..aac66e744 100644 --- a/app/view/netcreate/components/URComment.css +++ b/app/view/netcreate/components/URComment.css @@ -8,16 +8,24 @@ --comment-font-size-medium: 12px; --comment-font-weight-light: 300; --comment-font-weight-medium: 400; - --comment-status-font-color: #d44127; /* <= evan's, orig => #d84a0088; */ + --comment-status-font-color: #d84a0088; --comment-system-font-color: #0003; --disabled-opacity: 0.5; + --cmtbtn-read: #696969; /* svg symbols */ + --cmtbtn-unread-light: #ffe143; /* svg symbols */ + --cmtbtn-unread-dark: #d44127; /* svg symbols */ + --clr-comment: #ffe143; + --clr-comment-dark: #d44127; + --clr-comment-light: #ffe143; + --clr-comment-read-dark: #696969; + --clr-comment-read-light: #fff; } /* Comment Bar ------------------------------------------------------------- */ #comment-bar { display: flex; align-self: flex-start; - margin-top: 6px; + margin-top: 10px; font-size: 0.8rem; z-index: 2000; } @@ -64,6 +72,7 @@ max-width: 400px; overflow: hidden; text-overflow: ellipsis; + color: var(--comment-commenter-font-color); } #comment-bar button.small { background-color: #fff6; @@ -77,7 +86,10 @@ margin-left: 0.25rem; float: right; } -/* Comment Summary */ +/* Comment Status ---------------------------------------------------------- */ +#comment-status { +} +/* Comment Summary --------------------------------------------------------- */ #comment-summary { display: flex; color: var(--comment-status-font-color); @@ -87,7 +99,8 @@ line-height: 1.3rem; cursor: pointer; width: fit-content; - padding: 3px 7px; + padding: 1px 5px; + float: right; } #comment-summary.expanded { display: none; @@ -101,14 +114,13 @@ width: 20px; height: 20px; } -#comment-summary .comment-count, .comments-unread .comment-count { font-size: 10px; width: 20px; height: 20px; line-height: 20px; } -/* Comment Panel */ +/* Comment Panel ----------------------------------------------------------- */ #comment-panel { background-color: var(--comment-bg-light); border-radius: 15px; @@ -120,13 +132,16 @@ #comment-panel.expanded { display: block; height: fit-content; + /* pop out so so sidebars don't move */ + position: absolute; + margin-left: -300px; } -/* Comment Alert */ +/* Comment Alert ----------------------------------------------------------- */ #comment-alert { height: 2em; padding-right: 10px; line-height: 1.5rem; - overflow: hidden; + /* overflow: hidden; skip this? not necessary? */ opacity: 0.01; transform: translateY(-150%); } @@ -146,6 +161,11 @@ transform: translateY(0); transition: all 5s ease-in; } +#comment-alert .comment-status-body { + position: absolute; + top: 2px; + right: 10px; /* float to the left of the main status */ +} .comment-status-body { margin-left: 1.3rem; padding-bottom: 0.5rem; @@ -198,6 +218,7 @@ svg#comment-icon { height: 30px; font-size: 12px; font-weight: bold; + letter-spacing: -0.08em; line-height: 30px; position: relative; text-align: center; @@ -214,10 +235,6 @@ svg#comment-icon { .hasReadComments .comment-count { color: white; } -.commentbtn { - display: inline-block; - cursor: pointer; -} .comment-intable svg#comment-icon { width: 20px; height: 20px; @@ -225,7 +242,7 @@ svg#comment-icon { cursor: pointer; } -/* Comment Thread */ +/* Comment Thread ---------------------------------------------------------- */ .commentThread { display: flex; flex-direction: column; @@ -265,7 +282,7 @@ svg#comment-icon { .commentThread button { /* emulates nccomponent */ - color: #333; + color: var(--comment-commenter-font-color); background-color: #fff6; border-radius: 5px; border-color: #0006; @@ -279,10 +296,11 @@ svg#comment-icon { opacity: var(--disabled-opacity); } -/* Comment */ +/* Comment------------------------------------------------------------------ */ .comment { display: grid; grid-template-columns: 5em auto; + color: var(--comment-commenter-font-color); background-color: #fff5; margin-bottom: 5px; padding: 5px; @@ -292,6 +310,9 @@ svg#comment-icon { background-color: #fff8; } +.comment.unread { + border: 3px solid var(--comment-bg-new); +} .comment.deleted { background-color: #0000000a; } @@ -334,20 +355,21 @@ svg#comment-icon { .comment .label, .comment .help /* .help uses nccomponent */, .comment .commentId, -.comment-status-body .commenter { +.comment-status-body .commenter, +.comment .commentTypeBar .commentTypeLabel { color: #0008; font-size: var(--comment-font-size-medium); font-weight: var(--comment-font-weight-medium); + line-height: 14px; } .comment .label { - margin-top: 10px; + margin-top: 2px; } .commentThread .comment .help { /* override nccomponent */ font-style: italic; /* emulate nccomponent */ - font-size: 12px; + font-size: var(--comment-font-size-small); line-height: 14px; - padding-bottom: 3px; opacity: 0.7; } @@ -355,14 +377,22 @@ svg#comment-icon { overflow-y: auto; } +.comment .commentTypeBar { + display: flex; + column-gap: 5px; +} +.comment .commentTypeBar .commentTypeLabel { + flex-grow: 1; +} .comment .comment-item { - margin-bottom: 5px; + margin-bottom: 8px; } .comment .feedback, .comment .error { opacity: 0.7; - font-size: 12px; - line-height: 14px; + font-size: var(--comment-font-size-small); + line-height: var(--comment-font-size-medium); + margin: -2px 0 8px 0; } .comment .feedback { color: #0008; /* emulate nccomponent help */ @@ -380,7 +410,6 @@ svg#comment-icon { .comment .commentId { color: #0004; - float: right; } .comment .editmenu { @@ -411,11 +440,6 @@ svg#comment-icon { cursor: pointer; } -.commentbar, -.editbar { - padding-top: 5px; -} - .commentbar { display: flex; flex-direction: row-reverse; @@ -425,6 +449,7 @@ svg#comment-icon { display: flex; flex-direction: row; justify-content: flex-end; + padding-top: 5px; } .comment .commentbar button, @@ -439,10 +464,11 @@ svg#comment-icon { .comment .editbar button { margin-left: 15px; /* emulate nccomponent */ } + .comment .commentbar button:hover, .comment .editbar button:hover { color: white; - background-color: rgb(255, 141, 65); + background-color: #b2000b; } .comment .commentbar button.secondary, .comment .editbar button.secondary { @@ -467,6 +493,9 @@ svg#comment-icon { /* visibility: hidden; */ display: none; } +.comment:hover .commentbar { + padding-top: 5px; +} .comment:hover .commentbar button { /* visibility: visible; */ display: block; @@ -537,30 +566,81 @@ svg#comment-icon { border: 1px solid; border-color: var(--comment-status-font-color); } +.comment .prompt button.selected:disabled { + opacity: 0.9; +} .comment .prompt button.notselected { - opacity: 0.5; + opacity: 0.1; +} +.comment .prompt button.notselected:hover { + opacity: 0.3; } -/* URCommentVBtn */ -.URCommentVBtn { +/* URCommentCollectionMgr ----------------------------------------------------------- */ +.URCommentCollectionMgr { + position: absolute; +} + +/* SVG Comment Icons ------------------------------------------------------- */ +.commentbtn { + /* URCommentBtn & URCommentVBtn */ display: inline-block; cursor: pointer; - border: none; - background-color: transparent; } -.URCommentVBtn svg { - width: 24px; - height: 24px; +.commentbtn.disabled { + /* URCommentBtn & URCommentVBtn */ + cursor: default; } -.URCommentVBtn > .count { - height: 24px; - width: 24px; - margin-top: -21px; - color: #fff; - font-size: 10px; - font-weight: bold; +.commentbtn > .comment-count { + position: absolute; + line-height: 28px; + width: 32px; /* match size of URComentBtnAlias */ + height: 32px; text-align: center; + color: var(--cmtbtn-unread-dark); } -.URCommentVBtn > .count.unread { - color: var(--comment-status-font-color); +.commentbtn.hasUnreadComments .comment-count { + color: var(--cmtbtn-unread-dark); +} +.commentbtn.hasUnreadComments.isOpen .comment-count, +.commentbtn.hasReadComments.isOpen .comment-count { + color: white; +} +.commentbtn.hasReadComments .comment-count { + color: var(--cmtbtn-read); +} + +.svgcmt-read .svg-fill { + fill: var(--clr-comment-read-light); +} +.svgcmt-read .svg-outline { + fill: var(--clr-comment-read-light); +} + +.svgcmt-read-outlined .svg-fill { + fill: var(--clr-comment-read-light); +} +.svgcmt-read-outlined .svg-outline { + fill: var(--clr-comment-read-dark); +} + +.svgcmt-readSelected .svg-fill { + fill: var(--clr-comment-read-dark); +} +.svgcmt-readSelected .svg-outline { + fill: var(--clr-comment-read-dark); +} + +.svgcmt-unread .svg-fill { + fill: var(--clr-comment-light); +} +.svgcmt-unread .svg-outline { + fill: var(--clr-comment-dark); +} + +.svgcmt-unreadSelected .svg-fill { + fill: var(--clr-comment-dark); +} +.svgcmt-unreadSelected .svg-outline { + fill: var(--clr-comment-dark); } diff --git a/app/view/netcreate/components/URComment.jsx b/app/view/netcreate/components/URComment.jsx index 3fc899e04..9eee43e64 100644 --- a/app/view/netcreate/components/URComment.jsx +++ b/app/view/netcreate/components/URComment.jsx @@ -12,6 +12,15 @@ key={cvobj.comment_id} // part of thread array /> + 1. UI input cycle + URComment handles updates from by the URCommentPrompt component. + The data is stored locally until evt_SaveBtn is clicked, which then + calls comment-mgr.UpdateComment. + 2. Data State management + comment-mgr saves the data to the database + and updates COMMENTVOJBS state, which triggers a re-render of the + URCommentThread component. + \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ import React, { useState, useEffect, useCallback } from 'react'; @@ -41,6 +50,7 @@ const PR = 'URComment'; function URComment({ cref, cid, uid }) { const [element, setElement] = useState(null); const [state, setState] = useState({ + id: undefined, commenter: '', createtime_string: '', modifytime_string: '', @@ -70,10 +80,9 @@ function URComment({ cref, cid, uid }) { useEffect(() => { // declare helpers const urmsg_UpdatePermissions = data => { - const isLoggedIn = NetMessage.GlobalGroupID(); setState(prevState => ({ ...prevState, - uIsDisabled: data.commentBeingEditedByMe || !isLoggedIn + uIsDisabled: data.commentBeingEditedByMe })); }; const urstate_UpdateCommentVObjs = () => c_LoadCommentVObj(); @@ -95,13 +104,24 @@ function URComment({ cref, cid, uid }) { /** Declare helper method to load viewdata from comment manager into the * component state */ function c_LoadCommentVObj() { + // If the comment is being edited, skip the update, else we'd lose the edit + if (state.uIsBeingEdited) { + if (DBG) + console.log( + `COMMENTVOBJS Update! ${cid} is being edited skipping update!!!` + ); + return; + } + const cvobj = CMTMGR.GetCommentVObj(cref, cid); const comment = CMTMGR.GetComment(cid); // When deleting, COMMENTVOBJS state change will trigger a load and render // before the component is unmounted. So catch it and skip the state update. if (!cvobj || !comment) { - console.error('c_LoadCommentVObj: comment or cvobj not found!'); + console.log( + 'c_LoadCommentVObj: comment or cvobj not found! Usually because it has been deleted.' + ); return; } @@ -113,6 +133,8 @@ function URComment({ cref, cid, uid }) { // set component state from retrieved data setState({ // Data + // REVIEW MEME uses `comment.id` and NC uses `comment.comment_id` + id: comment.comment_id, // human readable "#xxx" id matching db id // MEME uses comment.id, matching pmcData id comment_id_parent: comment.comment_id_parent, commenter: CMTMGR.GetUserName(comment.commenter_id), selected_comment_type, @@ -126,7 +148,8 @@ function URComment({ cref, cid, uid }) { uIsSelected: cvobj.isSelected, uIsBeingEdited: cvobj.isBeingEdited, uIsEditable: cvobj.isEditable, - uAllowReply: cvobj.allowReply + uAllowReply: cvobj.allowReply, + uIsDisabled: CMTMGR.GetCommentsAreBeingEdited() // if I'm not editing, but someone else is, disable edit }); // Lock edit upon creation of a new comment or a new reply @@ -159,8 +182,7 @@ function URComment({ cref, cid, uid }) { ...prevState, uViewMode })); - - CMTMGR.LockComment(cid); + CMTMGR.UIEditComment(cid); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** handle save button, which saves the state to comment manager. @@ -171,8 +193,7 @@ function URComment({ cref, cid, uid }) { comment.comment_type = selected_comment_type; comment.commenter_text = [...commenter_text]; // clone, not byref comment.commenter_id = uid; - CMTMGR.UpdateComment(comment); - CMTMGR.UnlockComment(cid); + CMTMGR.UISaveComment(comment); setState(prevState => ({ ...prevState, uViewMode: CMTMGR.VIEWMODE.VIEW @@ -205,49 +226,55 @@ function URComment({ cref, cid, uid }) { /** handle delete button, which removes the comment associated with this * commment from the comment manager */ function evt_DeleteBtn() { + const { id } = state; CMTMGR.RemoveComment({ collection_ref: cref, + id, comment_id: cid, uid }); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** handle cancel button, which reverts the comment to its previous state, - * doing additional housekeeping to keep comment manager consistent */ + * doing additional housekeeping to keep comment manager consistent + * If the comment is empty and it's a new comment, just remove it + * */ function evt_CancelBtn() { - const { commenter_text } = state; - let savedCommentIsEmpty = true; - commenter_text.forEach(t => { - if (t !== '') savedCommentIsEmpty = false; - }); + const { commenter_text, id } = state; - const cb = () => CMTMGR.UnlockComment(cid); + let previouslyHadText = false; + CMTMGR.GetComment(cid).commenter_text.forEach(t => { + if (t !== '') previouslyHadText = true; + }); - if (savedCommentIsEmpty) { - // "Cancel" will always remove the comment if the comment is empty - // - usually because it's a newly created comment - // - but also if the user clears all the text fields - // We don't care if the user entered any text - CMTMGR.RemoveComment( - { - collection_ref: cref, - comment_id: cid, - uid, - showCancelDialog: true - }, - cb - ); - } else { - // revert to previous text if current text is empty + if (previouslyHadText) { + // revert to previous text + CMTMGR.UICancelComment(cid); const comment = CMTMGR.GetComment(cid); setState(prevState => ({ ...prevState, + modifytime_string: comment.modifytime_string, + selected_comment_type: comment.comment_type, commenter_text: [...comment.commenter_text], // restore previous text clone, not by ref + comment_error: '', uViewMode: CMTMGR.VIEWMODE.VIEW })); - - cb(); + } else { + // Remove the temporary comment and unlock + CMTMGR.RemoveComment({ + collection_ref: cref, + id, + comment_id: cid, + uid, + skipDialog: true + }); + setState({ + commenter_text: [], + uViewMode: CMTMGR.VIEWMODE.VIEW + }); } + + return; } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** handle select button, which updates the comment type associated with this @@ -342,7 +369,7 @@ function URComment({ cref, cid, uid }) { ))} ); - const SelectedType = commentTypes.get(selected_comment_type); //(type => type[0] === selected_comment_type); + const SelectedType = commentTypes.get(selected_comment_type); const SelectedTypeLabel = SelectedType ? SelectedType.label : 'Type not found'; // Alternative three-dot menu approach to hide "Edit" and "Delete" // const UIOnEditMenuSelect = event => { @@ -382,8 +409,10 @@ function URComment({ cref, cid, uid }) {
{modifytime_string || createtime_string}
-
#{cid}
-
{TypeSelector}
+
+
{TypeSelector}
+
#{comment.comment_id}
+
{commenter}
{modifytime_string || createtime_string}
-
#{cid}
-
- TYPE: - {SelectedTypeLabel} +
+
+ TYPE: + {SelectedTypeLabel} +
+
#{comment.comment_id}
- {uid && ( + {uid && !uIsDisabled && (
- {!uIsDisabled && !comment.comment_isMarkedDeleted && ReplyBtn} - {(!uIsDisabled && - isAllowedToEditOwnComment && + {!comment.comment_isMarkedDeleted && ReplyBtn} + {(isAllowedToEditOwnComment && !comment.comment_isMarkedDeleted && EditBtn) ||
} - {(((!uIsDisabled && - isAllowedToEditOwnComment && - !comment.comment_isMarkedDeleted) || + {(((isAllowedToEditOwnComment && !comment.comment_isMarkedDeleted) || isAdmin) && DeleteBtn) ||
}
diff --git a/app/view/netcreate/components/URCommentBtn.jsx b/app/view/netcreate/components/URCommentBtn.jsx index bcdd2ecbb..827e6d724 100644 --- a/app/view/netcreate/components/URCommentBtn.jsx +++ b/app/view/netcreate/components/URCommentBtn.jsx @@ -206,7 +206,7 @@ function URCommentBtn({ cref, uuiid }) { * - the "read" status of all comments: unread (gold) or read (gray) * - isOpen - click on the button to display threads in a new window */ - const count = CMTMGR.GetThreadedViewObjectsCount(cref, uid); + const count = CMTMGR.GetCommentCollectionCount(cref); const ccol = CMTMGR.GetCommentCollection(cref) || {}; let css = 'commentbtn '; diff --git a/app/view/netcreate/components/URCommentCollectionMgr.jsx b/app/view/netcreate/components/URCommentCollectionMgr.jsx new file mode 100644 index 000000000..c8a601d1b --- /dev/null +++ b/app/view/netcreate/components/URCommentCollectionMgr.jsx @@ -0,0 +1,146 @@ +/*//////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\ + + URCommentCollectionMgr + + Comment collection components are dynamically created and destroyed in the + DOM as the user requests opening and closing comment collection windows. + The URCommentCollectionMgr handles the insertion and removal of these + components. + + UR MESSAGES + * CMT_COLLECTION_SHOW {cref, position} + * CMT_COLLECTION_HIDE {cref} + * CMT_COLLECTION_HIDE_ALL + + USE: + + + + + HISTORY + Originally, URCommentBtn were designed to handle comment opening + in Net.Create from two types componets: nodes/edges and + NodeTables/EdgeTables. Since the requests could come + from different components, we had to keep track of which component + was requesting the opening, so they could close the corresponding + comment. In order to do this, we used a reference that combined + the comment id reference (collection reference, or cref) + with a unique user interface id (uuiid). + + MEME doesn't need that so we simply use `cref` as the + comment id. + + That simplifies the comment references, but there was a second + challenge: + + Since the SVG props and mechanisms are NOT react components, we + cannot use URCommentBtn to manage opening and editing comments. + Moreover, URCommentBtns opened inside Evidence Links are embedded + inside the Evidence Library and comments created inside the library + end up hidden due to layers of overflow divs. + + To get around this, URCommentCollectionMgr essentially replaces the + functionality of URCommentBtn with three pieces, acting as a middle + man and breaking out the... + * visual display -- URCommentSVGBtn + * UI click requests -- URCommentVBtn + * thread opening / closing requests -- URCommentCollectionMgr + ...into different functions handled by different components. + +\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ + +import React, { useState, useEffect, useCallback } from 'react'; +import UNISYS from 'unisys/client'; +// import UR from '../../../system/ursys'; +// const STATE = require('../lib/client-state'); + +import CMTMGR from '../comment-mgr'; +import URCommentThread from './URCommentThread'; + +/// CONSTANTS & DECLARATIONS ////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const DBG = true; +const PR = 'URCommentCollectionMgr'; + +const UDATAOwner = { name: 'URCommentCollectionMgr' }; +const UDATA = UNISYS.NewDataLink(UDATAOwner); + +/// REACT FUNCTIONAL COMPONENT //////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +function URCommentCollectionMgr(props) { + const uid = CMTMGR.GetCurrentUserId(); + const [cmtBtns, setCmtBtns] = useState([]); + const [dummy, setDummy] = useState(0); // Dummy state variable to force update + + /** Component Effect - register listeners on mount */ + useEffect(() => { + UDATA.OnAppStateChange('COMMENTVOBJS', redraw, UDATAOwner); + UDATA.HandleMessage('CMT_COLLECTION_SHOW', urmsg_COLLECTION_SHOW); + UDATA.HandleMessage('CMT_COLLECTION_HIDE', urmsg_COLLECTION_HIDE); + UDATA.HandleMessage('CMT_COLLECTION_HIDE_ALL', urmsg_COLLECTION_HIDE_ALL); + + return () => { + UDATA.AppStateChangeOff('COMMENTVOBJS', redraw); + UDATA.UnhandleMessage('CMT_COLLECTION_SHOW', urmsg_COLLECTION_SHOW); + UDATA.UnhandleMessage('CMT_COLLECTION_HIDE', urmsg_COLLECTION_HIDE); + UDATA.UnhandleMessage('CMT_COLLECTION_HIDE_ALL', urmsg_COLLECTION_HIDE_ALL); + }; + }, []); + + /// COMPONENT HELPER METHODS //////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + /// UR HANDLERS ///////////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + function redraw(data) { + // This is necessary to force a re-render of the threads + // when the comment collection changes on the net + // especially when a new comment is added. + setDummy(dummy => dummy + 1); // Trigger re-render + } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /** + * Handle urmsg_COLLECTION_SHOW message + * 1. Register the button, and + * 2. Open the URCommentBtn + * @param {Object} data + * @param {string} data.cref - Collection reference + * @param {Object} data.position - Position of the button + */ + function urmsg_COLLECTION_SHOW(data) { + if (DBG) console.log(PR, 'CMT_COLLECTION_SHOW', data); + setCmtBtns(prevBtns => [...prevBtns, data]); + } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + function urmsg_COLLECTION_HIDE(data) { + if (DBG) console.log(PR, 'CMT_COLLECTION_HIDE', data); + setCmtBtns(prevBtns => prevBtns.filter(btn => btn.cref !== data.cref)); + } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + function urmsg_COLLECTION_HIDE_ALL(data) { + if (DBG) console.log(PR, 'CMT_COLLECTION_HIDE_ALL', data); + setCmtBtns([]); + } + + /// COMPONENT RENDER //////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + return ( +
+ {cmtBtns.map(btn => ( + + ))} +
+ ); +} + +/// EXPORT REACT COMPONENT //////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +export default URCommentCollectionMgr; diff --git a/app/view/netcreate/components/URCommentPrompt.jsx b/app/view/netcreate/components/URCommentPrompt.jsx index 3d1fa6858..f9939e50a 100644 --- a/app/view/netcreate/components/URCommentPrompt.jsx +++ b/app/view/netcreate/components/URCommentPrompt.jsx @@ -189,6 +189,18 @@ function URCommentPrompt({ ); break; case 'dropdown': + if (!prompt.options.includes(commenterText[promptIndex])) { + // currently selected value does not match an item in the dropdown + // fall back to the first item in the dropdown + if (prompt.options.length > 0) + commenterText[promptIndex] = prompt.options[0]; + else { + console.warn( + `Dropdown for ${commentType} has no options! Check definition!` + ); + commenterText[promptIndex] = ''; // fall back to an empty string + } + } inputJSX = (