From 9b19365aa3a4748b5a7186c8cf8c26f0fd84478d Mon Sep 17 00:00:00 2001 From: benloh Date: Thu, 26 Sep 2024 14:02:38 -0700 Subject: [PATCH 01/65] match-meme-comment: Show comment type Match MEME --- app/view/netcreate/components/URComment.css | 18 +++++++++++++++--- app/view/netcreate/components/URComment.jsx | 6 ++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/view/netcreate/components/URComment.css b/app/view/netcreate/components/URComment.css index 1a94d6892..ad68dcdf9 100644 --- a/app/view/netcreate/components/URComment.css +++ b/app/view/netcreate/components/URComment.css @@ -4,6 +4,10 @@ --comment-bg-new: #d84a00; --comment-bg-new-light: #d84a0088; --comment-commenter-font-color: #333; + --comment-font-size-small: 10px; + --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-system-font-color: #0003; --disabled-opacity: 0.5; @@ -312,9 +316,16 @@ svg#comment-icon { fill: transparent; } -.comment .date { +.comment .date, +.comment .type { color: #0006; - font-size: 0.6em; + font-size: var(--comment-font-size-small); +} +.comment .date { + font-weight: var(--comment-font-weight-light); +} +.comment .type { + font-weight: var(--comment-font-weight-medium); } .commentThread .label, @@ -325,7 +336,8 @@ svg#comment-icon { .comment .commentId, .comment-status-body .commenter { color: #0008; - font-size: 0.8em; + font-size: var(--comment-font-size-medium); + font-weight: var(--comment-font-weight-medium); } .comment .label { margin-top: 10px; diff --git a/app/view/netcreate/components/URComment.jsx b/app/view/netcreate/components/URComment.jsx index 3816c9e6a..3fc899e04 100644 --- a/app/view/netcreate/components/URComment.jsx +++ b/app/view/netcreate/components/URComment.jsx @@ -342,6 +342,8 @@ function URComment({ cref, cid, uid }) { ))} ); + const SelectedType = commentTypes.get(selected_comment_type); //(type => type[0] === selected_comment_type); + const SelectedTypeLabel = SelectedType ? SelectedType.label : 'Type not found'; // Alternative three-dot menu approach to hide "Edit" and "Delete" // const UIOnEditMenuSelect = event => { // switch (event.target.value) { @@ -413,6 +415,10 @@ function URComment({ cref, cid, uid }) {
#{cid}
+
+ TYPE: + {SelectedTypeLabel} +
Date: Thu, 3 Oct 2024 12:33:45 -0700 Subject: [PATCH 02/65] match-meme-comment: Show simple "DELETED" for comment prompts Rather than show complex "DELETED" for every prompt in a comment type because there may be many. --- app/view/netcreate/components/URCommentPrompt.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/view/netcreate/components/URCommentPrompt.jsx b/app/view/netcreate/components/URCommentPrompt.jsx index 3d1fa6858..079600256 100644 --- a/app/view/netcreate/components/URCommentPrompt.jsx +++ b/app/view/netcreate/components/URCommentPrompt.jsx @@ -301,6 +301,8 @@ function URCommentPrompt({ const RenderViewMode = () => { const NOTHING_SELECTED = (nothing selected); + if (isMarkedDeleted) return
DELETED
; + return commentTypes.get(commentType).prompts.map((prompt, promptIndex) => { let displayJSX; switch (prompt.format) { From 191091a9dccd15b051cbe103b2b7eb361d670c6d Mon Sep 17 00:00:00 2001 From: benloh Date: Thu, 3 Oct 2024 13:59:20 -0700 Subject: [PATCH 03/65] match-meme-comment: Fix bug: `commenter_id` is inadvertently changed when updating comment status. --- .../netcreate/components/URCommentStatus.jsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/view/netcreate/components/URCommentStatus.jsx b/app/view/netcreate/components/URCommentStatus.jsx index b5121cd41..1a3e4c9c1 100644 --- a/app/view/netcreate/components/URCommentStatus.jsx +++ b/app/view/netcreate/components/URCommentStatus.jsx @@ -78,15 +78,22 @@ function URCommentStatus(props) { const my_uaddr = UNISYS.SocketUADDR(); const isNotMe = my_uaddr !== uaddr; - if (comment && comment.commenter_text.length > 0) { + // clone the comment object so we can modify the reply text + // otherwise we inadvertently change the commenter_id + const status_comment = Object.assign({}, comment); + if ( + status_comment && + status_comment.commenter_text && + status_comment.commenter_text.length > 0 + ) { let source; - if (comment.comment_id_parent) { - source = `${comment.commenter_id} replied: `; + if (status_comment.comment_id_parent) { + source = `${status_comment.commenter_id} replied: `; } else { - source = `${comment.commenter_id} commented: `; + source = `${status_comment.commenter_id} commented: `; } - comment.commenter_id = source; - const message = c_GetCommentItemJSX(comment); + status_comment.commenter_id = source; + const message = c_GetCommentItemJSX(status_comment); setMessages(prevMessages => [...prevMessages, message]); // Only show status update if it's coming from another From 3bc96977d41d474e756d9fab14ddcde93e50e9bf Mon Sep 17 00:00:00 2001 From: benloh Date: Fri, 4 Oct 2024 10:19:39 -0700 Subject: [PATCH 04/65] match-meme-comment: Count only non-deleted comments. Add `commentCount` to TCommentCollection --- _ur_addons/comment/ac-comment.ts | 14 ++++++++------ app/view/netcreate/comment-mgr.js | 8 +++++--- app/view/netcreate/components/NCEdgeTable.jsx | 2 +- app/view/netcreate/components/NCNodeTable.jsx | 2 +- app/view/netcreate/components/URCommentBtn.jsx | 2 +- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/_ur_addons/comment/ac-comment.ts b/_ur_addons/comment/ac-comment.ts index 3ea3546f2..9a3bbbb62 100644 --- a/_ur_addons/comment/ac-comment.ts +++ b/_ur_addons/comment/ac-comment.ts @@ -13,12 +13,14 @@ * 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; } @@ -68,8 +70,6 @@ isMarkedRead: boolean; isReplyToMe: boolean; allowReply: boolean; - - markedRead: boolean; } \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * /////////////////////////////////////*/ @@ -96,6 +96,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 +114,8 @@ type TCommentVisualObject = { isBeingEdited: boolean; isEditable: boolean; isMarkedRead: boolean; - isReplyToMe: boolean; - allowReply: boolean; - markedRead: boolean; + isReplyToMe?: boolean; + allowReply?: boolean; }; type TCommentCollectionMap = Map; @@ -318,11 +318,13 @@ function DeriveThreadedViewObjects( if (cref === undefined) throw new Error(`m_DeriveThreadedViewObjects cref: "${cref}" must be defined!`); const commentVObjs = []; + 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 +369,7 @@ function DeriveThreadedViewObjects( }); ccol.hasUnreadComments = hasUnreadComments; ccol.hasReadComments = hasReadComments; + ccol.commentCount = commentCount; COMMENTCOLLECTION.set(cref, ccol); return commentReplyVObjs; } @@ -582,7 +585,6 @@ export { DeriveAllThreadedViewObjects, DeriveThreadedViewObjects, GetThreadedViewObjects, - GetThreadedViewObjectsCount, GetCOMMENTVOBJS, GetCommentVObj, // Comment Objects diff --git a/app/view/netcreate/comment-mgr.js b/app/view/netcreate/comment-mgr.js index 2947bd341..9a2c74be8 100644 --- a/app/view/netcreate/comment-mgr.js +++ b/app/view/netcreate/comment-mgr.js @@ -355,6 +355,11 @@ MOD.CloseCommentCollection = (uiref, cref, uid) => { UDATA.LocalCall('CTHREADMGR_THREAD_CLOSED', { cref }); }; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +MOD.GetCommentCollectionCount = cref => { + const ccol = COMMENT.GetCommentCollection(cref); + return ccol ? ccol.commentCount : ''; +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MOD.GetCommentStats = () => { const uid = MOD.GetCurrentUserId(); return COMMENT.GetCommentStats(uid); @@ -415,9 +420,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 diff --git a/app/view/netcreate/components/NCEdgeTable.jsx b/app/view/netcreate/components/NCEdgeTable.jsx index ea80fefac..fe90d5a16 100644 --- a/app/view/netcreate/components/NCEdgeTable.jsx +++ b/app/view/netcreate/components/NCEdgeTable.jsx @@ -667,7 +667,7 @@ class NCEdgeTable extends UNISYS.Component { // 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); diff --git a/app/view/netcreate/components/NCNodeTable.jsx b/app/view/netcreate/components/NCNodeTable.jsx index 1b1d32bc0..aa497258e 100644 --- a/app/view/netcreate/components/NCNodeTable.jsx +++ b/app/view/netcreate/components/NCNodeTable.jsx @@ -583,7 +583,7 @@ class NCNodeTable extends UNISYS.Component { // 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); 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 '; From 0c35f2536b3b6b31e8749b553b40fb23434e99ea Mon Sep 17 00:00:00 2001 From: benloh Date: Fri, 4 Oct 2024 11:20:08 -0700 Subject: [PATCH 05/65] match-meme-comment: Ignore deleted comments in comment stats (unread, repliesToMe) --- _ur_addons/comment/ac-comment.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/_ur_addons/comment/ac-comment.ts b/_ur_addons/comment/ac-comment.ts index 9a3bbbb62..860e614a4 100644 --- a/_ur_addons/comment/ac-comment.ts +++ b/_ur_addons/comment/ac-comment.ts @@ -232,14 +232,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++; + } } } }); From 44fbfaa33aad5680095f70e5f424f4b6afbe77db Mon Sep 17 00:00:00 2001 From: benloh Date: Sat, 14 Dec 2024 09:48:57 -0800 Subject: [PATCH 06/65] match-meme-comment: Backport URTable --- app/view/netcreate/NetCreate.css | 1 + app/view/netcreate/components/NCEdgeTable.jsx | 41 +-- app/view/netcreate/components/NCNodeTable.jsx | 35 +- app/view/netcreate/components/URTable.css | 41 ++- app/view/netcreate/components/URTable.jsx | 305 +++++++++++++++--- 5 files changed, 324 insertions(+), 99 deletions(-) diff --git a/app/view/netcreate/NetCreate.css b/app/view/netcreate/NetCreate.css index 5f8da6ab0..183be6351 100644 --- a/app/view/netcreate/NetCreate.css +++ b/app/view/netcreate/NetCreate.css @@ -117,4 +117,5 @@ button svg { left: 1px; right: 10px; background-color: #eafcff; + font-size: 0.75em; } diff --git a/app/view/netcreate/components/NCEdgeTable.jsx b/app/view/netcreate/components/NCEdgeTable.jsx index fe90d5a16..3d68524cd 100644 --- a/app/view/netcreate/components/NCEdgeTable.jsx +++ b/app/view/netcreate/components/NCEdgeTable.jsx @@ -4,8 +4,8 @@ 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 displays NCDATA. But also read FILTEREDNCDATA to show highlight/filtered + state ## PROPS @@ -76,7 +76,6 @@ class NCEdgeTable extends UNISYS.Component { disableEdit: false, isLocked: false, isExpanded: true, - sortkey: 'Relationship', dummy: 0, // used to force render update COLUMNDEFS: [] @@ -377,21 +376,6 @@ class NCEdgeTable extends UNISYS.Component { 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) { @@ -446,7 +430,8 @@ class NCEdgeTable extends UNISYS.Component { } /// RENDERERS /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function RenderViewOrEdit(value) { + function RenderViewOrEdit(key, tdata, coldef) { + const value = tdata[key]; return (
{!disableEdit && ( @@ -473,9 +458,11 @@ class NCEdgeTable extends UNISYS.Component { // id: String; // label: String; // } - function RenderNode(value) { + function RenderNode(key, tdata, coldef) { + const value = tdata[key]; + // console.log('RenderNode', value); if (!value) return; // skip if not defined yet - if (value.id === undefined || value.label === undefined) { + if (value.id === undefined || value === undefined) { // During Edge creation, source/target may not be defined yet return ...; } @@ -489,7 +476,8 @@ class NCEdgeTable extends UNISYS.Component { ); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function RenderCommentBtn(value) { + function RenderCommentBtn(key, tdata, coldef) { + const value = tdata[key]; return ( {!disableEdit && ( @@ -431,23 +428,25 @@ class NCNodeTable extends UNISYS.Component { // id: String; // label: String; // } - function RenderNode(value) { + function 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) { + function RenderCommentBtn(key, tdata, coldef) { + const value = tdata[key]; return ( tr, .URTable thead > tr > th { user-select: none; } +.URTable th span.sortDisabled { + display: none; +} +.URTable th span.sortEnabled { + display: none; +} +.URTable th:hover { + background-color: var(--bg-hover-clr); + cursor: default; +} +.URTable th:hover span { + opacity: 0.5; +} +.URTable th:hover span.sortEnabled { + display: inline; + cursor: pointer; +} +.URTable th.selected span { + display: inline; +} +.URTable th.selected:hover span { + opacity: 1; +} + .URTable tr:hover { - background-color: #0002; + background-color: var(--bg-hover-clr); +} +.URTable td { + padding: 0 var(--padding-horizontal); } .URTable .resize-handle { position: absolute; diff --git a/app/view/netcreate/components/URTable.jsx b/app/view/netcreate/components/URTable.jsx index 2705c8c18..76c11ff33 100644 --- a/app/view/netcreate/components/URTable.jsx +++ b/app/view/netcreate/components/URTable.jsx @@ -1,10 +1,114 @@ /*///////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\ + UR Table -Implements a table with resizable columns. +Implements a table with resizable and sortable columns. Emulates the API of Handsontable. +Used also on https://github.com/netcreateorg/netcreate-itest/ + +# API - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +## Renderers + This emulates Handsontable's renderer concept. + * A renderer is a function that takes a value and returns a JSX element + * If a column has a renderer, it will be used to render the value in the cell + +## Sorters + This emulates Handsontable's sorter concept. + * A sorter is a function that takes a key, the table data, and the sort order + * If a column has a sorter, it will be used to sort the table data + +## Column Widths + * Column widths can be set in the column definition + * Columns without a width will be evenly distributed + + +# User Interaction - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +## Resizing Columns + * Columns can be resized by dragging the right edge of the column header + The column to the right of the dragged edge will expand or contract + * Max resize -- Resizing is done between two columns: the current column, + and the column to the right. (We assume the table width is fixed or 100%). + To give you simple, but finer control over resizing columns, you can expand or + contract the current column and the next column up/down to a minimum size. + Once you hit the max size, you need to resize other columns to adjust. + (We don't allow you to resize neighboring columns once you've hit the + max/min just to simplify the math). + +## Sort Order + By default the table is unsorted. + * Clicking on a column header will sort the table by that column in ascending order + * Subsequent clicks will toggle to descending to unsorted and back to ascending + * The next sort order is highlighted on hover with a transparent arrow + * The column remembers the previous sort order when another column is selected + so re-selecting the column will restore the previous sort order + * A column can be designated unsortable by setting `sortDisabled` to `true` + + +# PROPS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + `tableData`: array + The data to be displayed in the table. + + [ + { id: 1, label: 'Constantinople', founding: '100 ce' }, + { id: 2, label: 'Athens', founding: '1000 bce' }, + { id: 3, label: 'Cairo', founding: '3000 bce' }, + ] + + `isOpen`: boolean + To optimize performance, the column widths are only calculated + when it's open. + `columns`: array + Specifications for how each column is to be rendered and sorted. + + Column Definition + title: string + data: string + type: 'text' | 'text-case-insensitive' | 'number' | 'timestamp' | 'markdown' | 'hdate' + width: number in px + renderer: (value: any) => JSX.Element + sorter: (key: string, tdata: any[], order: number) => any[] + sortDisabled: boolean + + Example usage: + + const COLUMNDEFS = [ + { + title: 'TITLE', + data: 'title', + type: 'text', + width: 300, // in px + renderer: this.RendererTitle, + sorter: (key, tdata, order) => { + const sortedData = [...tdata].sort((a, b) => { + // note `title` is stuffed into `tdata` + if (a[key].title < b[key].title) return order; + if (a[key].title > b[key].title) return order * -1; + return 0; + }); + return sortedData; + } + }, + { + title: 'UPDATED', + data: 'dateModified', + type: 'text', + width: 300 // in px + }, + ] + + const TABLEDATA = modelsWithGroupLabels.map(model => { + return { + id: model.id, + title: model.title, + groupLabel: model.groupLabel, + dateModified: HumanDate(model.dateModified), + dateCreated: HumanDate(model.dateCreated) + }; + }); \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * /////////////////////////////////////*/ @@ -17,23 +121,33 @@ import HDATE from 'system/util/hdate'; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const DBG = false; +const SORTORDER = new Map(); +SORTORDER.set(0, '▲▼'); +SORTORDER.set(1, '▲'); +SORTORDER.set(-1, '▼'); + /// FUNCTIONAL COMPONENT DECLARATION ////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function URTable({ isOpen, data, columns }) { - const [tabledata, setTableData] = useState([]); - const [columndefs, setColumnDefs] = useState([]); - const [columnWidths, setColumnWidths] = useState([]); - const [sortColumnIdx, setSortColumnIdx] = useState(0); - const [sortOrder, setSortOrder] = useState(1); + const [_tabledata, setTableData] = useState([]); + const [_columndefs, setColumnDefs] = useState([]); + const [_columnWidths, setColumnWidths] = useState([]); + const [_sortColumnIdx, setSortColumnIdx] = useState(0); + const [_sortOrder, setSortOrder] = useState(0); + const [_previousColSortOrder, setPreviousColSortOrder] = useState({}); - const tableRef = useRef(null); - const resizeRef = useRef(null); + const ref_Table = useRef(null); + const ref_Resize = useRef(null); /// USE EFFECT ////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Init table data useEffect(() => { setTableData(data); + // default to ascending when a column is first clicked + const defaultSortOrders = {}; + columns.forEach((item, idx) => (defaultSortOrders[idx] = -1)); + setPreviousColSortOrder(defaultSortOrders); }, []); /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Calculate Initial Column Widths @@ -61,23 +175,37 @@ function URTable({ isOpen, data, columns }) { useEffect(() => { // Sort table data m_ExecuteSorter(data); - }, [sortColumnIdx, sortOrder]); + }, [_sortColumnIdx, _sortOrder, _previousColSortOrder]); /// UTILITIES /////////////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + function u_HumanDate(timestamp) { + if (timestamp === undefined || timestamp === '') return ''; + const date = new Date(timestamp); + const timestring = date.toLocaleTimeString('en-Us', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + const datestring = date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric' + }); + return `${datestring} ${timestring}`; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const u_CalculateColumnWidths = () => { - // if table is not drawn yet, skip - if (tableRef.current.clientWidth < 1) return; + if (ref_Table.current.clientWidth < 1) return; // if it's already set, don't recalculate - if (columnWidths.length > 0 && tableRef.current.clientWidth > 0) { + if (_columnWidths.length > 0 && ref_Table.current.clientWidth > 0) { return; } const definedColWidths = columns.filter(col => col.width).map(col => col.width); const definedColWidthSum = definedColWidths.reduce((a, b) => a + b, 0); - const remainingWidth = tableRef.current.clientWidth - definedColWidthSum; + const remainingWidth = ref_Table.current.clientWidth - definedColWidthSum; const colWidths = columns.map( col => col.width || remainingWidth / (columns.length - definedColWidths.length) ); @@ -89,20 +217,20 @@ function URTable({ isOpen, data, columns }) { const ui_MouseDown = (event, index) => { event.preventDefault(); event.stopPropagation(); - resizeRef.current = { + ref_Resize.current = { index, startX: event.clientX, - startWidth: columnWidths[index], - nextStartWidth: columnWidths[index + 1], - maxCombinedWidth: columnWidths[index] + columnWidths[index + 1] - 50 + startWidth: _columnWidths[index], + nextStartWidth: _columnWidths[index + 1], + maxCombinedWidth: _columnWidths[index] + _columnWidths[index + 1] - 50 }; }; const ui_MouseMove = event => { - if (resizeRef.current !== null) { + if (ref_Resize.current !== null) { const { index, startX, startWidth, nextStartWidth, maxCombinedWidth } = - resizeRef.current; + ref_Resize.current; const delta = event.clientX - startX; - const newWidths = [...columnWidths]; + const newWidths = [..._columnWidths]; newWidths[index] = Math.min(Math.max(50, startWidth + delta), maxCombinedWidth); // Minimum width set to 50px newWidths[index + 1] = Math.min( Math.max(50, nextStartWidth - delta), @@ -112,16 +240,35 @@ function URTable({ isOpen, data, columns }) { } }; const ui_MouseUp = () => { - resizeRef.current = null; // Reset on mouse up + ref_Resize.current = null; // Reset on mouse up }; /// CLICK HANDLERS ////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function ui_ClickSorter(event, index) { + function ui_SetSelectedColumn(event, index) { event.preventDefault(); event.stopPropagation(); + if (_columndefs[index].sortDisabled) return; // If sort is disabled, do nothing + + if (_sortColumnIdx === index) { + // if already selected, toggle the sort order + let newSortOrder; + if (_sortOrder === 0) newSortOrder = -1; + else if (_sortOrder > 0) newSortOrder = 0; + else newSortOrder = 1; + setSortOrder(newSortOrder); + } else { + // otherwise default to the previous order + setSortOrder(_previousColSortOrder[index]); + } + + // update the previous sort order + setPreviousColSortOrder({ + ..._previousColSortOrder, + [_sortColumnIdx]: _sortOrder + }); + setSortColumnIdx(index); - setSortOrder(sortOrder * -1); } /// BUILT-IN SORTERS //////////////////////////////////////////////////////// @@ -138,6 +285,17 @@ function URTable({ isOpen, data, columns }) { }); return sortedData; } + function m_SortCaseInsensitive(key, tdata, order) { + const sortedData = [...tdata].sort((a, b) => { + if (!a[key] && !b[key]) return 0; + if (!a[key]) return 1; // Move undefined or '' to the bottom regardless of sort order + if (!b[key]) return -1; // Move undefined or '' the bottom regardless of sort order + if (a[key].toLowerCase() < b[key].toLowerCase()) return order; + if (a[key].toLowerCase() > b[key].toLowerCase()) return order * -1; + return 0; + }); + return sortedData; + } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_SortByMarkdown(key, tdata, order) { const sortedData = [...tdata].sort((a, b) => { @@ -197,26 +355,30 @@ function URTable({ isOpen, data, columns }) { /// BUILT-IN TABLE METHODS ////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /** - * - * @param {*} value - * @param {*} col - * @param {*} idx + * Executes the renderer for a given column + * @param {string} key tdata object key + * @param {Object} tdata full table data object so you can access other keys + * @param {Object} coldef column definition * @returns The final value to be rendered in the table cell */ - function m_ExecuteRenderer(value, col, idx) { - const customRenderer = col.renderer; + function m_ExecuteRenderer(key, tdata, coldef) { + const customRenderer = coldef.renderer; if (customRenderer) { if (typeof customRenderer !== 'function') - throw new Error('Invalid renderer for', col); - return customRenderer(value); + throw new Error('Invalid renderer for', coldef); + return customRenderer(key, tdata, coldef); } else { // Run built-in renderers - switch (col.type) { - case 'markdown': + const value = tdata[key]; + switch (coldef.type) { + case 'markdown': // Net.Create return value.html; - case 'hdate': + case 'hdate': // Net.Create + case 'timestamp': + return u_HumanDate(value); case 'number': case 'text': + case 'text-case-insensitive': default: return value; } @@ -225,67 +387,104 @@ function URTable({ isOpen, data, columns }) { /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Sorts then sets the table data function m_ExecuteSorter(tdata) { - if (columndefs.length < 1) return []; + if (_columndefs.length < 1) return []; - const customSorter = columndefs[sortColumnIdx].sorter; - const key = columndefs[sortColumnIdx].data; + const customSorter = _columndefs[_sortColumnIdx].sorter; + const key = _columndefs[_sortColumnIdx].data; let sortedData = []; if (customSorter) { if (typeof customSorter !== 'function') throw new Error('Invalid sorter'); - sortedData = customSorter(key, tdata, sortOrder); + sortedData = customSorter(key, tdata, _sortOrder); } else { // Run built-in sorters - switch (columndefs[sortColumnIdx].type) { + switch (_columndefs[_sortColumnIdx].type) { case 'hdate': - sortedData = m_SortByHDate(key, tdata, sortOrder); + sortedData = m_SortByHDate(key, tdata, _sortOrder); break; case 'markdown': - sortedData = m_SortByMarkdown(key, tdata, sortOrder); + sortedData = m_SortByMarkdown(key, tdata, _sortOrder); break; - case 'date': case 'number': - sortedData = m_SortByNumber(key, tdata, sortOrder); + sortedData = m_SortByNumber(key, tdata, _sortOrder); + break; + case 'text-case-insensitive': + sortedData = m_SortCaseInsensitive(key, tdata, _sortOrder); break; + case 'timestamp': // timestamp is a string case 'text': default: - sortedData = m_SortByText(key, tdata, sortOrder); + sortedData = m_SortByText(key, tdata, _sortOrder); } } setTableData(sortedData); } /// RENDER ////////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + /** + * jsx_SortBtn tracks two states: + * if sort is disabled: + * - do not show sort button in header + * - the header is not clickable (no cursor pointer) + * if sort is enabled: + * - show sort erder: '▲' or '▼' or '▲▼' + * - show cursor pointer + */ + function jsx_SortBtn(columndef, idx) { + const isSelected = _sortColumnIdx === idx; + if (columndef.sortDisabled) { + return -; + } else if (isSelected) { + console.log('selected sort order', _sortOrder); + return {SORTORDER.get(_sortOrder)}; + } else { + // not selected, so sort order is the previous sort order + return ( + + {SORTORDER.get(_previousColSortOrder[idx])} + + ); + } + } + + // show cursor pointer if not sortDisabled + // needs to be set at `th` not at `th span` + return (
- {columndefs.map((col, idx) => ( + {_columndefs.map((coldef, idx) => ( ))} - {tabledata.map((tdata, idx) => ( + {_tabledata.map((tdata, idx) => ( + /* Net.Create Special Handling: Show filter transparency */ - {columndefs.map((col, idx) => ( - + {_columndefs.map((coldef, idx) => ( + ))} ))} From bc4b11a4da3a433a2c7fcf62069d86b64b01847b Mon Sep 17 00:00:00 2001 From: benloh Date: Mon, 23 Dec 2024 11:32:19 -0800 Subject: [PATCH 07/65] Merge branch 'dev-next' into dev-bl/match-meme-comment -- missing NCTemplate.jsx --- app/view/netcreate/components/NCTemplate.jsx | 348 +++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 app/view/netcreate/components/NCTemplate.jsx diff --git a/app/view/netcreate/components/NCTemplate.jsx b/app/view/netcreate/components/NCTemplate.jsx new file mode 100644 index 000000000..2139ceeca --- /dev/null +++ b/app/view/netcreate/components/NCTemplate.jsx @@ -0,0 +1,348 @@ +/* eslint-disable no-alert */ +/*//////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\ + + NC Template Editor View + (replaces `Template.jsx`) + + Displays a variety of tools to edit templates: + * Edit Node Types + * Edit Edge Types + * Download Current Template + * Create New Template + * Import Template from File + + This is displayed on the More.jsx component/panel but can be moved + anywhere. + + Templates can only be edited if: + * There are no nodes or edges being edited + * No one is trying to import data + * There are no other templates being edited + + Conversely, if a Template is being edited, Import, Node and Edge editing + will be disabled. + + ## BACKGROUND + + Template data is loaded by `server-database` DB.InitializeDatabase call. + +\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ + +const React = require('react'); +const UNISYS = require('unisys/client'); +const { EDITORTYPE } = require('system/util/enum'); +const TEMPLATE_MGR = require('../templateEditor-mgr'); +const SCHEMA = require('../template-schema'); +const DATASTORE = require('system/datastore'); + +/// CONSTANTS & DECLARATIONS ////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const DBG = false; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +let UDATA = null; + +/// REACT COMPONENT /////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +class NCTemplate extends UNISYS.Component { + constructor(props) { + super(props); + this.state = { + disableEdit: false, + isBeingEdited: false, + editScope: undefined, // Determines whether the user is tring to edit the + // template's root (everything in the template), + // or just focused on a subsection: nodeTypeOptions, + // edgeTypeOptions + tomlfile: undefined, + tomlfileStatus: '', + tomlfileErrors: undefined, + tomlfilename: 'loading...' + }; + this.updateEditState = this.updateEditState.bind(this); + this.disableOrigLabelFields = this.disableOrigLabelFields.bind(this); + this.releaseOpenEditor = this.releaseOpenEditor.bind(this); + this.onNewTemplate = this.onNewTemplate.bind(this); + this.onCurrentTemplateLoad = this.onCurrentTemplateLoad.bind(this); + this.onEditNodeTypes = this.onEditNodeTypes.bind(this); + this.onEditEdgeTypes = this.onEditEdgeTypes.bind(this); + this.onTOMLfileSelect = this.onTOMLfileSelect.bind(this); + this.onDownloadTemplate = this.onDownloadTemplate.bind(this); + this.onSaveChanges = this.onSaveChanges.bind(this); + this.onCancelEdit = this.onCancelEdit.bind(this); + + UDATA = UNISYS.NewDataLink(this); + UDATA.HandleMessage('EDIT_PERMISSIONS_UPDATE', this.updateEditState); + } // constructor + + componentDidMount() { + this.updateEditState(); + DATASTORE.GetTemplateTOMLFileName().then(result => { + this.setState({ tomlfilename: result.filename }); + }); + } + + componentWillUnmount() { + this.releaseOpenEditor(); + UDATA.UnhandleMessage('EDIT_PERMISSIONS_UPDATE', this.updateEditState); + } + + /// UI EVENT HANDLERS ///////////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + updateEditState() { + // 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 || data.nodeOrEdgeBeingEdited; + this.setState({ disableEdit }); + }); + } + + // When editing Node or Edge Type Options, the original label field should be + // disabled so they can't be edited + // ClassName added in template-schema.GetTypeEditorSchema() + disableOrigLabelFields() { + const origLabelFields = document.getElementsByClassName('disabledField'); + // origLabelFields is a HTMLCollection, not an array + // FISHY FIX...is the use of arrow function here correct? The arrow function + // arg 'f' is shadowing the 'const f' in the for...of... + for (const f of origLabelFields) f => f.setAttribute('disabled', 'disabled'); + } + + releaseOpenEditor() { + UDATA.NetCall('SRV_RELEASE_EDIT_LOCK', { editor: EDITORTYPE.TEMPLATE }); + } + + onNewTemplate() { + this.setState({ editScope: 'root', isBeingEdited: true }); + this.loadEditor(); // new blank template with default schema + } + + onCurrentTemplateLoad(e) { + UDATA.LocalCall('EDIT_CURRENT_TEMPLATE') // nc-logic + .then(result => { + this.setState({ editScope: 'root', isBeingEdited: true }); + this.loadEditor({ startval: result.template }); + }); + } + + onEditNodeTypes() { + UDATA.LocalCall('EDIT_CURRENT_TEMPLATE') // nc-logic + .then(result => { + const schemaNodeTypeOptions = SCHEMA.NODETYPEOPTIONS; + // Wrap options in custom Schema to show Delete management UI + const nodeTypeEditorSchema = + SCHEMA.GetTypeEditorSchema(schemaNodeTypeOptions); + const startval = { options: result.template.nodeDefs.type.options }; + this.setState({ editScope: 'nodeTypeOptions', isBeingEdited: true }); + this.loadEditor( + { + schema: nodeTypeEditorSchema, + startval + }, + () => { + this.disableOrigLabelFields(); + // HACK: After a row is added, we need to also disable the newly added + // "Label" field -- the new label should be added in the "Change To" field + EDITOR.on('addRow', editor => { + this.disableOrigLabelFields(); + }); + } + ); + }); + } + + onEditEdgeTypes() { + UDATA.LocalCall('EDIT_CURRENT_TEMPLATE') // nc-logic + .then(result => { + const schemaEdgeTypeOptions = SCHEMA.EDGETYPEOPTIONS; + // Wrap options in custom Schema to show Delete management UI + const edgeTypeEditorSchema = + SCHEMA.GetTypeEditorSchema(schemaEdgeTypeOptions); + const startval = { options: result.template.edgeDefs.type.options }; + this.setState({ editScope: 'edgeTypeOptions', isBeingEdited: true }); + this.loadEditor( + { + schema: edgeTypeEditorSchema, + startval + }, + () => { + this.disableOrigLabelFields(); + // HACK: After a row is added, we need to also disable the newly added + // "Label" field -- the new label should be added in the "Change To" field + EDITOR.on('addRow', editor => { + this.disableOrigLabelFields(); + }); + } + ); + }); + } + + onTOMLfileSelect(e) { + // import + const tomlfile = e.target.files[0]; + TEMPLATE_MGR.ValidateTOMLFile({ tomlfile }).then(result => { + if (result.isValid) { + console.log('got template', result.templateJSON); + this.onSaveChanges(result.templateJSON); + } else { + const errorMsg = result.error; + this.setState({ + tomlfile: undefined, + tomlfileStatus: 'Invalid template file!!!', + tomlfileErrors: errorMsg + }); + } + }); + } + + onDownloadTemplate() { + TEMPLATE_MGR.DownloadTemplate(); + } + + onSaveChanges(templateJSON) { + TEMPLATE_MGR.SaveTemplateToFile(templateJSON).then(result => { + console.error('onSaveChanges', result, templateJSON); + if (!result.OK) { + alert(result.info); + } else { + alert(`Template Saved: ${templateJSON.name}`); + this.setState({ isBeingEdited: false }); + } + }); + this.releaseOpenEditor(); + } + + onCancelEdit() { + this.setState({ isBeingEdited: false }); + this.releaseOpenEditor(); + } + + /// REACT LIFECYCLE METHODS /////////////////////////////////////////////////// + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + render() { + const { + disableEdit, + isBeingEdited, + tomlfile, + tomlfileStatus, + tomlfileErrors, + tomlfilename + } = this.state; + let editorjsx; + + if (disableEdit && !isBeingEdited) { + // Node or Edge is being edited, show disabled message + editorjsx = ( +
+

+ + Templates cannot be edited while someone is editing a node, edge, or + template, or importing data. + +

+

+ Please finish editing and try again. +

+
+ ); + } else { + // OK to Edit, show edit buttons + editorjsx = ( + + ); + } + return ( +
+

Template Editor

+

+ {tomlfilename} +

+ {editorjsx} + + +
+ ); + } +} + +/// EXPORT REACT COMPONENT //////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +module.exports = NCTemplate; From e7dd1eca6c7a6599cbd5471e6d6404f7e127cd74 Mon Sep 17 00:00:00 2001 From: benloh Date: Tue, 31 Dec 2024 09:09:04 -0800 Subject: [PATCH 08/65] fix-export: Fail gracefully if node type options or edge type options have not been defined. --- app/view/netcreate/nc-logic.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/view/netcreate/nc-logic.js b/app/view/netcreate/nc-logic.js index 15dfb8b00..4a3782363 100644 --- a/app/view/netcreate/nc-logic.js +++ b/app/view/netcreate/nc-logic.js @@ -937,15 +937,19 @@ function m_UpdateColorMap() { // someone ever chooses to use the same label twice, but ... try { const nodeColorMap = {}; - TEMPLATE.nodeDefs.type.options.forEach(o => { - nodeColorMap[o.label] = o.color; - }); + if (TEMPLATE.nodeDefs.type && TEMPLATE.nodeDefs.type.options) { + TEMPLATE.nodeDefs.type.options.forEach(o => { + nodeColorMap[o.label] = o.color; + }); + } const edgeColorMap = {}; let defaultEdgeColor = TEMPLATE.edgeDefs.color || '#999'; //for backwards compatability - TEMPLATE.edgeDefs.type.options.forEach(o => { - edgeColorMap[o.label] = o.color || defaultEdgeColor; - }); + if (TEMPLATE.edgeDefs.type && TEMPLATE.edgeDefs.type.options) { + TEMPLATE.edgeDefs.type.options.forEach(o => { + edgeColorMap[o.label] = o.color || defaultEdgeColor; + }); + } UDATA.SetAppState('COLORMAP', { nodeColorMap, edgeColorMap }); } catch (error) { From 7075a116e803a9d61dc3791a1b8b3cadf3b64945 Mon Sep 17 00:00:00 2001 From: benloh Date: Tue, 31 Dec 2024 09:16:55 -0800 Subject: [PATCH 09/65] match-meme-comment: Match MEME URCommentPrompt --- .../netcreate/components/URCommentPrompt.jsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/view/netcreate/components/URCommentPrompt.jsx b/app/view/netcreate/components/URCommentPrompt.jsx index 079600256..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 = (
-
ui_ClickSorter(e, idx)}>{col.title}
+
ui_SetSelectedColumn(e, idx)}> + {coldef.title}  + {jsx_SortBtn(coldef, idx)} +
ui_MouseDown(e, idx)} - hidden={idx === columndefs.length - 1} // hide last resize handle + hidden={idx === _columndefs.length - 1} // hide last resize handle >
{m_ExecuteRenderer(tdata[col.data], col, idx)}{m_ExecuteRenderer(coldef.data, tdata, coldef)}